HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<section>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<div class="flexContainer">
<div class="cardContainer">
<div class="perspective-card" data-decorator="PerspectiveCard" data-ambient>
<div class="perspective-card__transformer">
<div class="perspective-card__artwork perspective-card__artwork--front">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/9b1b5b5-1.png" />
</div>
<div class="perspective-card__artwork perspective-card__artwork--rear">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/pokemon_card_backside_in_high_resolution_by_atomicmonkeytcg_dah43cy-fullview.png" />
</div>
<div class="perspective-card__shine"></div>
</div>
</div>
</div>
<div class="cardContainer">
<div class="perspective-card" data-decorator="PerspectiveCard" data-ambient>
<div class="perspective-card__transformer">
<div class="perspective-card__artwork perspective-card__artwork--front">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/9b1b5b5-1.png" />
</div>
<div class="perspective-card__artwork perspective-card__artwork--rear">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/pokemon_card_backside_in_high_resolution_by_atomicmonkeytcg_dah43cy-fullview.png" />
</div>
<div class="perspective-card__shine"></div>
</div>
</div>
</div>
<div class="cardContainer">
<div class="perspective-card" data-decorator="PerspectiveCard" data-ambient>
<div class="perspective-card__transformer">
<div class="perspective-card__artwork perspective-card__artwork--front">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/9b1b5b5-1.png" />
</div>
<div class="perspective-card__artwork perspective-card__artwork--rear">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/pokemon_card_backside_in_high_resolution_by_atomicmonkeytcg_dah43cy-fullview.png" />
</div>
<div class="perspective-card__shine"></div>
</div>
</div>
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</section>
.flexContainer {
justify-items: center;
display: flex;
position: relative;
}
section {
margin: 50px auto;
width: 900px;
}
.cardContainer {
height: 418px;
width: 300px;
}
.perspective-card {
height: 418px;
perspective: 350px;
position: relative;
touch-action: none;
transform-style: preserve-3d;
width: 300px;
}
.perspective-card__shine {
border-radius: 15px;
left: 0px;
height: 100%;
position: absolute;
top: 0px;
transform: translateZ(.5px);
width: 100%;
}
.perspective-card__transformer {
height: 100%;
position: relative;
transform-style: preserve-3d;
width: 100%;
}
.perspective-card__artwork {
position: absolute;
top: 0;
}
.perspective-card__artwork--front {
}
.perspective-card__artwork--rear {
transform: translateZ(-.5px) scaleX(-1);
}
.perspective-card img {
border-radius: 15px;
box-shadow: 5px 5px 0px rgba(0,0,0,.0);
display: block;
max-width: 100%;
transition: .5s linear box-shadow;
}
.perspective-card--over img {
box-shadow: 5px 5px 20px rgba(0,0,0,.4);
}
.perspective-card--matte {
background-color: rgba(0,0,0,.5);
height: 100vh;
left: 0;
opacity: 0;
position: fixed;
top: 0;
transition: opacity .5s linear;
width: 100vw;
z-index: 1000;
}
.perspective-card--matte.modal {
opacity: 1;
}
.perspective-card.modal {
z-index: 1001;
}
console.clear();
const SUPPORTSTOUCH = "ontouchstart" in window || navigator.msMaxTouchPoints;
const EPSILON = 0.001;
// Easing functions
const easeInOutCubic = function(time, start, change, duration) {
if ((time /= duration / 2) < 1)
return change * 0.5 * time * time * time + start;
return change * 0.5 * ((time -= 2) * time * time + 2) + start;
};
const easeInOutSine = function(time, start, change, duration) {
return (-change / 2) * (Math.cos((Math.PI * time) / duration) - 1) + start;
};
/**
* This sets up the basic perspective card. This class expects markup at least
* conforming to:
* ```
* .perspective-card
* .perspective-card__transformer
* .perspective-card__artwork card__artwork--front
* img
* .perspective-card__artwork card__artwork--rear (optional)
* img
* .perspective-card__shine
* ```
*
* This class is designed to be used with a decorator function (provided by
* the new wtc-decorator static class) or used directly like:
* ```
* const p = new PerspectiveCard(element);
* ```
*
* @author Liam Egan <liam@wethecollective.com>
* @version 2.0.0
* @created Jan 28, 2020
*/
class PerspectiveCard {
/**
* The PerspectiveCard constructor. Creates and initialises the perspective card component.
*
* @constructor
* @param {HTMLElement} element The element that contains all of the card details
* @param {Object} settings The settings of the component
*/
constructor(element, settings = {}) {
// Set the element
this.element = element;
// set settings
this.settings = {
ambient:
settings.ambient || this.element.hasAttribute("data-ambient") || false,
debug: settings.debug || this.element.hasAttribute("data-debug") || false
};
// Find the transformer and shine elements. We save these so we
// don't waste proc time doing it every frame
this.transformer = this.element.querySelector(
".perspective-card__transformer"
);
this.shine = this.element.querySelector(".perspective-card__shine");
// Bind our event listeners
this.resize = this.resize.bind(this);
this.pointerMove = this.pointerMove.bind(this);
this.pointerEnter = this.pointerEnter.bind(this);
this.pointerLeave = this.pointerLeave.bind(this);
this.play = this.play.bind(this);
this.intersect = this.intersect.bind(this);
// Add event listeners for resize, scroll, pointer enter and leave
window.addEventListener("resize", this.resize);
window.addEventListener("scroll", this.resize);
this.element.addEventListener("pointerenter", this.pointerEnter);
this.element.addEventListener("pointerleave", this.pointerLeave);
if (this.settings.ambient) {
// Set up and bind the intersection observer
this.observer = new IntersectionObserver(this.intersect, {
rootMargin: "0%",
threshold: [0.1]
});
this.observer.observe(this.element);
}
// Initial resize to find the location and dimensions of the element
this.resize();
}
/**
* This is the main run-loop function.
* It is responsible for taking the various previously set properies
* and transforming the card. This can be called individually, or
* (more commonly) as the callback to a animation frame.
*
* @public
* @param {number} delta The delta of the animation
* @param {boolean} raf=true This just determines whether to run the next RAF as a part of this call
*/
play(delta, raf = true) {
// If `playing` is true, then request the animation frame again
if (this.playing && raf === true) {
requestAnimationFrame(this.play);
}
// Set the last frame time in order to derive the sensible delta
this.lastFrameTime = Math.max(16, Math.min(32, delta - this.lastDelta));
this.lastDelta = delta;
this.delta += this.lastFrameTime;
// Set the divisor for animations based on the last frame time
let divisor = 1 / this.lastFrameTime;
// if (isNaN(divisor) || divisor === Infinity) divisor = 1;
// If this element is not pointer controlled then we want to animate
// the ambient target point value around somehow. Here we use a simple
// fourier simulation.
if (!this.pointerControlled) {
// const d = delta * 0.0005;
// const a = 1.8 + Math.sin(2. * d + .2) + .4 * Math.cos(4. * 2. * d);
// const l = a * 80.;
const d = this.delta * 0.0001;
const s = Math.sin(d * 2);
const c = Math.cos(d * 0.5);
const l = 200 * Math.cos(d * 3.542 + 1234.5); // Some really arbitrary numbers here. They don't mean anythign in particular, they just work.
this.tPoint = [c * l, s * l, this.tPoint[2]];
}
// If our zoom differential (the different between the zoom and
// target zoom) is greater than the EPS value. We should animate it
if (Math.abs(this.zoom - this.center[2]) > EPSILON) {
this.center = [
this.center[0],
this.center[1],
this.center[2] + (this.zoom - this.center[2]) * (divisor * 2)
];
}
// If our look differential (the difference between the look
// point and the target point) is greater than 2 then we should
// animate it. We use a relatively arbitrary value of 2 here
// because we're using the square of the distance (to save
// unecessary calculation) here.
if (this._lookDifferential > 2) {
this.lookPoint = [
this.lookPoint[0] +
(this.tPoint[0] - this.lookPoint[0]) * (divisor * 2),
this.lookPoint[1] +
(this.tPoint[1] - this.lookPoint[1]) * (divisor * 2),
this.lookPoint[2] + (this.tPoint[2] - this.lookPoint[2]) * (divisor * 2)
];
}
// Find the wold matrix using the targetTo method (see above)
const worldMatrix = PerspectiveCard.targetTo(this.center, this.lookPoint, [
0,
1,
0
]);
// Find the polar coordinates for the rendition of the gradient.
const angle =
Math.atan2(this.lookPoint[1], this.lookPoint[0]) + Math.PI * 0.5;
const len = Math.hypot(this.lookPoint[0], this.lookPoint[1]);
// Transform the transformer element using the calculated values
const matrix = `matrix3d(${worldMatrix[0]},${worldMatrix[1]},${worldMatrix[2]},${worldMatrix[3]},${worldMatrix[4]},${worldMatrix[5]},${worldMatrix[6]},${worldMatrix[7]},${worldMatrix[8]},${worldMatrix[9]},${worldMatrix[10]},${worldMatrix[11]},${worldMatrix[12]},${worldMatrix[13]},${worldMatrix[14]},${worldMatrix[15]})`;
this.transformer.style.transform = matrix;
// Draw the gradient using the polar coordinates.
this.shine.style.background = `linear-gradient(${angle}rad, rgba(255,255,255,${Math.max(
0.01,
Math.abs(len * 0.002)
)}) 0%, rgba(255,255,255,${Math.max(
0.01,
Math.abs(len * 0.002)
)}) 5%, rgba(255,255,255,0) 80%)`;
}
/**
* Calculates the difference between the look point and the look point target
*
* @public
*/
calculateLookDifferential() {
const d = [
this.lookPoint[0] - this.tPoint[0],
this.lookPoint[1] - this.tPoint[1],
this.lookPoint[2] - this.tPoint[2]
];
this._lookDifferential = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
}
/**
* Event Listeners
*/
/**
* The event listener for the pointer move event.
* This sets the target point to a value based on the pointer's position
*
* @public
* @param {event} e The pointer event object
* @listens pointermove
*/
pointerMove(e) {
this.tPoint = [
e.clientX - this.axis[0],
e.clientY - this.axis[1],
this.tPoint[2]
];
}
/**
* The event listener for the pointer enter event
* This sets the pointerControlled property to true, updates the target
* zoom and adds the class `perspective-card--over` to the element.
*
* @public
* @param {event} e The pointer event object
* @listens pointerenter
*/
pointerEnter(e) {
this.pointerControlled = true;
this.zoom = 40;
this.element.classList.add("perspective-card--over");
}
/**
* The event listener for the pointer leave event
* This sets the pointerControlled property to false, updates the
* target zoom and removes the class `perspective-card--over` to the element.
*
* @public
* @param {event} e The pointer event object
* @listens pointerleave
*/
pointerLeave(e) {
this.pointerControlled = false;
this.zoom = 0;
this.element.classList.remove("perspective-card--over");
}
/**
* The event listener for the resize and scroll events
* This updates the position and size of the element and sets the
* axis for use in animation. This is bound to a debouncer so that
* it doesn't get called a hundred times when scrolling or
* resizing.
*
* @public
* @param {event} e The pointer event object
* @listens pointerleave
* @listens scroll
*/
resize(e) {
const resize = () => {
const pos = this.element.getBoundingClientRect();
this.position = [pos.left, pos.top];
this.size = [this.element.offsetWidth, this.element.offsetHeight];
this.axis = [
this.position[0] + this.size[0] * 0.5,
this.position[1] + this.size[1] * 0.5
];
};
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(resize, 300);
}
/**
* Listener for the intersection observer callback
*
* @public
* @param {object} entries the object that contains all of the elements being calculated by this observer
* @param {object} observer the observer instance itself
* @return void
*/
intersect(entries, observer) {
// Loop through the entries and set up the playing state based on whether the element is onscreen or not.
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
this.playing = true;
} else {
this.playing = false;
}
});
}
/**
* Getters and setters
*/
/**
* (getter/setter) The element value
*
* @type {HTMLElement}
* @default null
*/
set element(value) {
if (value instanceof HTMLElement) this._element = value;
}
get element() {
return this._element || null;
}
/**
* (getter/setter) The position of the element relative to the viewport.
*
* @type {Array}
* @default [0, 0]
*/
set position(value) {
if (value instanceof Array && value.length >= 2) {
this._position = value;
}
}
get position() {
return this._position || [0, 0];
}
/**
* (getter/setter) The 3D target look point. This is the point that the
* look point will animate towards.
*
* @type {Array}
* @default [0, 0, -800]
*/
set tPoint(value) {
if (value instanceof Array && value.length >= 3) {
this._tPoint = value;
this.calculateLookDifferential();
}
}
get tPoint() {
return this._tPoint || [0, 0, -800];
}
/**
* (getter/setter) The 3D look point. This is the point that the card
* look look at.
*
* @type {Array}
* @default [0, 0, -800]
*/
set lookPoint(value) {
if (value instanceof Array && value.length >= 3) {
this.calculateLookDifferential();
this._lookPoint = value;
}
}
get lookPoint() {
return this._lookPoint || [0, 0, -800];
}
/**
* (getter/setter) The 3D point that the card sits at.
*
* @type {Array}
* @default [0, 0, 0]
*/
set center(value) {
if (value instanceof Array && value.length >= 3) {
this._center = value;
}
}
get center() {
return this._center || [0, 0, 0];
}
/**
* (getter/setter) The target zoom value. If this is very different to the
* Z component of the center point, the animation frame will attempt to
* animate towards this.
*
* @type {Array}
* @default [0, 0, 0]
*/
set zoom(value) {
if (!isNaN(value)) this._zoom = value;
}
get zoom() {
return this._zoom || 0;
}
/**
* (getter/setter) The size of the element.
*
* @type {Array}
* @default [0, 0]
*/
set size(value) {
if (value instanceof Array && value.length >= 2) {
this._size = value;
}
}
get size() {
return this._size || [0, 0];
}
/**
* (getter/setter) The axis of the element relative to the top-left point.
*
* @type {Array}
* @default [0, 0]
*/
set axis(value) {
if (value instanceof Array && value.length >= 2) {
this._axis = value;
}
}
get axis() {
return this._axis || [0, 0];
}
/**
* (getter/setter) Whether the simulation is playing. Setting this to
* true will start up a requestAnimationFrame with the `play` method.
*
* @type {Boolean}
* @default false
*/
set playing(value) {
if (!this.playing && value === true) {
// Reset last frame time
this.lastFrameTime = 0;
requestAnimationFrame(this.play);
}
this._playing = value === true;
}
get playing() {
return this._playing === true;
}
/**
* (getter/setter) The amount of time the last frame took
*
* @type {Number}
* @default 0
*/
set lastFrameTime(value) {
if (!isNaN(value)) this._lastframeTime = value;
}
get lastFrameTime() {
return this._lastframeTime || 0;
}
/**
* (getter/setter) The animation delta. We use this and not the
* RaF delta because we want this to pause when the animation is
* not running.
*
* @type {Number}
* @default 0
*/
set delta(value) {
if (!isNaN(value)) this._delta = value;
}
get delta() {
return this._delta || 0;
}
/**
* (getter/setter) The animation's last frame delta delta.
*
* @type {Number}
* @default 0
*/
set lastDelta(value) {
if (!isNaN(value)) this._lastDelta = value;
}
get lastDelta() {
return this._lastDelta || 0;
}
/**
* (getter/setter) Whether the card animates based on the position
* of the pointer. If this is true it will set the pointermove
* event listener, otherwise it will try to remove it.
*
* @type {Boolean}
* @default false
*/
set pointerControlled(value) {
if (!this.pointerControlled && value === true) {
window.addEventListener("pointermove", this.pointerMove);
} else if (this.pointerControlled && value === false) {
window.removeEventListener("pointermove", this.pointerMove);
}
this._pointerControlled = value === true;
}
get pointerControlled() {
return this._pointerControlled === true;
}
/**
* Static classes
*/
/**
* Generates a matrix that makes something look at something else.
*
* @static
* @param {vec3} eye Position of the viewer
* @param {vec3} center Point the viewer is looking at
* @param {vec3} up vec3 pointing up
* @returns {mat4} out
*/
static targetTo(eye, target, up) {
if (eye.array) eye = eye.array;
if (target.array) target = target.array;
if (up.array) up = up.array;
if (
eye.length &&
eye.length >= 3 &&
target.length &&
target.length >= 3 &&
up.length &&
up.length >= 3
) {
const e = { x: eye[0], y: eye[1], z: eye[2] },
c = { x: target[0], y: target[1], z: target[2] },
u = { x: up[0], y: up[1], z: up[2] };
const off = {
x: e.x - c.x,
y: e.y - c.y,
z: e.z - c.z
};
let l = off.x * off.x + off.y * off.y + off.z * off.z;
if (l > 0) {
l = 1 / Math.sqrt(l);
off.x *= l;
off.y *= l;
off.z *= l;
}
const or = {
x: u.y * off.z - u.z * off.y,
y: u.z * off.x - u.x * off.z,
z: u.x * off.y - u.y * off.x
};
l = or.x * or.x + or.y * or.y + or.z * or.z;
if (l > 0) {
l = 1 / Math.sqrt(l);
or.x *= l;
or.y *= l;
or.z *= l;
}
return [
or.x,
or.y,
or.z,
0,
off.y * or.z - off.z * or.y,
off.z * or.x - off.x * or.z,
off.x * or.y - off.y * or.x,
0,
off.x,
off.y,
off.z,
0,
e.x,
e.y,
e.z,
1
];
}
}
}
/**
* The clickable perspective card adds functionality that allows the zooming
* the card by clicking on it. In doing so the card flips and animates up to a
* modal style display.
*
* @todo Add some extra functionality here like a close button and keyboard close
*
* @author Liam Egan <liam@wethecollective.com>
* @version 2.0.0
* @created Jan 28, 2020
* @extends PerspectiveCard
*/
class ClickablePerspectiveCard extends PerspectiveCard {
/**
* The ClickablePerspectiveCard constructor. Creates and initialises the perspective
* card component.
*
* @constructor
* @param {HTMLElement} element The element that contains all of the card details
* @param {Object} settings The settings of the component
*/
constructor(element, settings) {
// Call the superfunction
super(element, settings);
// Bind the extra handlers
this.onClick = this.onClick.bind(this);
this.onKey = this.onKey.bind(this);
// Add the listener to the pointer up event
this.element.addEventListener("pointerup", this.onClick);
// Set the card's starting dimensions
this.startingDimensions = [
this.element.offsetWidth,
this.element.offsetHeight
];
// Create the matte - this is the element that will appear behind the card.
this.matte = document.createElement("div");
this.matte.className = `${this.element.classList[0]}--matte`;
}
/**
* This is the main run-loop function.
* It is responsible for taking the various previously set properies
* and transforming the card. This can be called individually, or
* (more commonly) as the callback to a animation frame.
*
* @public
* @param {number} delta The delta of the animation
* @param {boolean} raf=true This just determines whether to run the next RAF as a part of this call
*/
play(delta, raf = true) {
// Call the superfunction
super.play(delta, raf);
// If we are tweening values and our tween time is less than the duration
if (this.tweenTime < this.tweenDuration && this.tweening === true) {
// Tween the position of the card on screen
this.screenPosition = [
easeInOutCubic(
this.tweenTime,
this.startingPosition[0],
this.targetPosition[0] - this.startingPosition[0],
this.tweenDuration
),
easeInOutCubic(
this.tweenTime,
this.startingPosition[1],
this.targetPosition[1] - this.startingPosition[1],
this.tweenDuration
)
];
// Tween the card scale
this.screenScale = easeInOutCubic(
this.tweenTime,
this.startingScale,
this.targetScale - this.startingScale,
this.tweenDuration
);
// Tween the rotation value
// This is responsible for moving the look at point in a large circle
// around the card and gives the illusion that the card is flipping
const r = easeInOutSine(
this.tweenTime,
Math.PI * 0.5,
this.rotationAmount,
this.tweenDuration
);
const t = [Math.cos(r) * -800, Math.sin(r) * -800];
this.lookPoint = [t[0], this.lookPoint[1], t[1]];
// Update the tween time with the last frame duration
this.tweenTime += this.lastFrameTime;
// Resize things so that mouse interation is sensible
this.resize();
// If our time has run out, but tweening is true it means that the animation has just ended
} else if (this.tweening === true) {
// Set the card's position on screen to the fixed end point
this.screenPosition = this.targetPosition;
this.tweening = false;
// Run our end function.
this.onEndTween();
}
}
// Toggle the enlarged flag on click
onClick() {
this.enlarged = !this.enlarged;
}
onKey(e) {
console.log(e.keyCode);
if (e.keyCode === 27) this.enlarged = false;
}
/**
* (getter/setter) Whether the card is enlarged or not. This is a BIG
* setter and is really responsible for generating the tweening values
* setting up the tween and initialising it.
*
* @type {Boolean}
* @default false
*/
set enlarged(value) {
// Whether we were enlarged already
const wasEnlarged = this.enlarged;
// Set the value
this._enlarged = value === true;
console.log(this.enlarged === false, wasEnlarged === true)
// If we're going from unenlarged to enlarged
if (this.enlarged === true && wasEnlarged === false) {
window.addEventListener("keyup", this.onKey);
const viewportOffset = this.element.getBoundingClientRect();
// Set up the DOM for this. Basically the same as setting up a modal.
document.body.style.overflow = "hidden";
if (
["MacIntel", "iPhone", "iPad", "Android"].indexOf(
navigator.platform
) === -1
)
document.body.style.paddingRight = "15px"; // Restricting this to non macs
this.element.style.position = "fixed";
this.element.classList.add("modal");
setTimeout(() => {
this.matte.classList.add("modal");
}, 0);
document.body.appendChild(this.matte);
// Initialise our tween timing variables
this.tweening = true;
this.tweenTime = 0;
this.tweenDuration = 1500; // 1.5 seconds
// Set up our positional arrays
// Start position
this.startingPosition = [
viewportOffset.left,
viewportOffset.top
];
// Current position
this.screenPosition = [viewportOffset.left, viewportOffset.top];
// End position
this.targetPosition = [
window.innerWidth * 0.5 - this.startingDimensions[0] * 0.5,
window.innerHeight * 0.5 - this.startingDimensions[1] * 0.5
];
// Set up our scaling properties
// start scale
this.startingScale = 1;
// current scale
this.screenScale = 1;
// Then we need to determine the target position based on the ratio of the screen to the card
// This basically ensures that we scale up to 70% width *or* 70% height. Whichever is smaller
const screenRatio = window.innerWidth / window.innerHeight;
const cardRatio = this.startingDimensions[0] / this.startingDimensions[1];
if (screenRatio < cardRatio) {
const width = window.innerWidth * 0.7;
this.targetScale = width / this.startingDimensions[0];
} else {
const height = window.innerHeight * 0.7;
this.targetScale = height / this.startingDimensions[1];
}
// Set up the amount of rotation that needs to happen
this.rotationAmount = Math.PI * -2;
// An empty endTween function for this tween
this.onEndTween = function() {};
// If we're going from enlarged to unenlarged
} else if (this.enlarged === false && wasEnlarged === true) {
window.removeEventListener("keyup", this.onKey);
// Remove the modal class from the matte
this.matte.classList.remove("modal");
// Initialise our tween timing variables
this.tweening = true;
this.tweenTime = 0;
this.tweenDuration = 1000; // 1 second
// Set up our positional arrays. Basically just opposing the previous tween
const startingPosition = this.startingPosition;
this.startingPosition = this.targetPosition;
this.targetPosition = startingPosition;
// Set up our scaling properties
this.startingScale = this.screenScale;
this.targetScale = 1;
// Set up the amount of rotation that needs to happen
// We want this to be opposite to the previous one
this.rotationAmount = Math.PI * 2;
// At the end of this tween we clean everything up
this.onEndTween = function() {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
this.element.classList.remove("modal");
document.body.removeChild(this.matte);
this.element.style.position = "";
this.screenPosition = [0, 0];
this.element.style.left = "";
this.element.style.top = "";
};
}
}
get enlarged() {
return this._enlarged === true;
}
/**
* (getter/setter) Whether the card is in a tweening state. This just
* enforces a boolean value.
*
* @type {Boolean}
* @default false
*/
set tweening(value) {
this._tweening = value === true;
}
get tweening() {
return this._tweening === true;
}
/**
* (getter/setter) The current tween time.
*
* @type {Number}
* @default 0
*/
set tweenTime(value) {
if (!isNaN(value)) this._tweenTime = value;
}
get tweenTime() {
return this._tweenTime || 0;
}
/**
* (getter/setter) The current tween duration.
*
* @type {Number}
* @default 0
*/
set tweenDuration(value) {
if (!isNaN(value)) this._tweenDuration = value;
}
get tweenDuration() {
return this._tweenDuration || 0;
}
/**
* (getter/setter) The function to call when the tween ends.
*
* @type {Function}
* @default null
*/
set onEndTween(value) {
if (value instanceof Function) {
this._onEndTween = value.bind(this);
}
}
get onEndTween() {
return this._onEndTween || function() {};
}
/**
* (getter/setter) The target position on-screen for the card.
*
* @type {Vec2|Array}
* @default [0,0]
*/
set targetPosition(value) {
if (value instanceof Array && value.length >= 2) {
this._targetPosition = value;
}
}
get targetPosition() {
return this._targetPosition || [0, 0];
}
/**
* (getter/setter) The current position on-screen for the card.
* This also updates the element's styles left and top. So this
* should *only* be set during a tween.
*
* @type {Vec2|Array}
* @default [0,0]
*/
set screenPosition(value) {
if (value instanceof Array && value.length >= 2) {
this._screenPosition = value;
this.element.style.left = `${value[0]}px`;
this.element.style.top = `${value[1]}px`;
}
}
get screenPosition() {
return this._screenPosition || [0, 0];
}
/**
* (getter/setter) The card's current scale value.
*
* @type {Number}
* @default 0
*/
set screenScale(value) {
if (!isNaN(value)) {
this._screenScale = value;
this.element.style.transform = `scale(${value})`;
}
}
get screenScale() {
return this._screenScale || 1;
}
/**
* (getter/setter) The target dimensions for the card.
*
* @type {Vec2|Array}
* @default [0,0]
*/
set targetDimensions(value) {
if (value instanceof Array && value.length >= 2) {
this._targetDimensions = value;
}
}
get targetDimensions() {
return this._targetDimensions || [0, 0];
}
}
const decorate = function(decorator, nodeSet) {
const controllers = [];
Array.from(nodeSet).forEach((node) => {
const controller = new decorator(node, node.dataset);
node.data = node.data || {};
node.data.controller = controller;
controllers.push(controller);
});
return controllers;
}
const controllers = decorate(ClickablePerspectiveCard, document.querySelectorAll('[data-decorator="PerspectiveCard"]'));
// export { PerspectiveCard }
Also see: Tab Triggers