<div class="main">
<div class="main__inner">
<h1 class="main__ttl">
<a href="https://nocebo.jp/post-1184/" target="_blank">ZoomSlider[拡大機能付きのスライダー]</a>
</h1>
<div class="slider">
<nav class="slider__nav">
<a href="javascript:;" class="slider__prev"><i class="fas fa-angle-left"></i></a>
<a href="javascript:;" class="slider__zoom"><i class="fas fa-search-plus"></i></a>
<a href="javascript:;" class="slider__next"><i class="fas fa-angle-right"></i></a>
</nav>
<div class="slider__inner">
<div class="slider__main">
<ul class="slider__list">
<li class="slider__elm slider__elm--01">
<a href="javascript:;">
<img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
</a>
</li>
<li class="slider__elm slider__elm--02">
<a href="javascript:;">
<img src="https://nocebo.jp/data/zoomslider/img/02.jpg" alt="images">
</a>
</li>
<li class="slider__elm slider__elm--03">
<a href="javascript:;">
<img src="https://nocebo.jp/data/zoomslider/img/03.jpg" alt="images">
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- ====================================__ .slider ============================= -->
<div class="modal">
<nav class="modal__nav">
<a href="javascript:;" class="modal__close"><i class="fas fa-times"></i></a>
</nav>
<div class="modal__elm">
<div class="zoom">
<div class="zoom__info">
<img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
<div class="zoom__origin zoom__origin--02"></div>
<div class="zoom__area"></div>
<div class="zoom__data">
<p class="zoom__op">origin<span></span></p>
<p class="zoom__cp">move<span></span></p>
<p class="zoom__scale">scale<span></span></p>
</div>
</div>
<div class="zoom__elm">
<div class="zoom__origin zoom__origin--01"></div>
<img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
</div>
</div>
<!-- ====================================__ .zoom ============================= -->
</div>
</div>
<!-- ====================================__ .modal ============================= -->
</div>
</div>
<!-- ====================================__ .main ============================= -->
*
{
box-sizing: border-box;
text-decoration: none;
touch-action: none;
}
img
{
user-select: none;
pointer-events: none;
}
$baseWidth: 768px;
.contents
{
height: 100%;
}
.main
{
display: flex;
width: $baseWidth;
min-height: 100%;
margin: 0 auto;
background-color: #eee;
align-items: center;
.main__inner
{
padding: 50px 0;
}
.main__ttl
{
font-size: 30px;
font-weight: bold;
margin-bottom: 20px;
margin-left: 10px;
color: #000;
}
}
.slider
{
position: relative;
width: $baseWidth;
.slider__inner
{
overflow: hidden;
width: 100%;
}
.slider__elm
{
img
{
vertical-align: bottom;
}
}
.slider__nav
{
position: absolute;
z-index: 10;
bottom: 0;
display: flex;
width: 100%;
justify-content: space-between;
a
{
font-size: 30px;
font-weight: bold;
display: block;
width: 33.2%;
padding: 25px;
padding-bottom: 20px;
text-align: center;
text-decoration: none;
color: #000;
background-color: rgba(#fff, .7);
&:hover
{
background-color: rgba(#fff, 1);
}
}
}
.slider__list
{
display: flex;
width: max-content;
li
{
width: $baseWidth;
img
{
width: 100%;
pointer-events: none;
}
}
}
}
.modal
{
position: fixed;
z-index: -1;
top: 0;
left: 0;
visibility: hidden;
width: 100%;
height: 100%;
transition: all .2s linear;
opacity: 0;
background-color: rgba(0, 0, 0, .9);
&.active
{
z-index: 1000;
visibility: visible;
opacity: 1;
}
.modal__nav
{
position: absolute;
top: 30px;
right: 20px;
}
.modal__close
{
font-size: 50px;
position: fixed;
z-index: 10000;
top: 10px;
right: 10px;
width: 80px;
height: 80px;
padding: 23px;
padding-top: 15px;
color: #000;
border-radius: 3px;
border-radius: 100%;
background-color: #fff;
}
.modal__elm
{
display: flex;
height: 100%;
align-items: center;
}
}
.zoom
{
position: relative;
overflow: hidden;
width: $baseWidth;
height: $baseWidth;
margin: 0 auto;
border: 1px solid #ccc;
.zoom__elm
{
width: 100%;
&:hover
{
cursor: grab;
}
img
{
width: 100%;
vertical-align: bottom;
}
}
.zoom__info
{
position: absolute;
z-index: 10;
top: 1%;
left: 1%;
width: $baseWidth / 5;
height: $baseWidth / 5;
border: 1px solid #ccc;
background-color: rgba(255, 255, 255, .4);
img
{
width: 100%;
opacity: .5;
filter: grayscale(50%);
}
}
.zoom__origin
{
position: absolute;
z-index: 100;
top: 0;
left: 0;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
border-radius: 100%;
background-color: rgba(255, 0, 0, .5);
&.zoom__origin--02
{
width: 10px;
height: 10px;
margin-top: -5px;
margin-left: -5px;
}
}
.zoom__area
{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform-origin: 100% 100%;
border: 1px solid #ccc;
background-color: rgba(0, 0, 0, .3);
}
.zoom__data
{
font-size: 15px;
position: absolute;
top: -1px;
left: $baseWidth / 5 + 10;
width: 200px;
text-align: left;
color: #fff;
p
{
line-height: 1.3;
margin-bottom: 5px;
}
span
{
display: inline-block;
margin-left: 5px;
}
}
}
View Compiled
// /* ===========================================================
// # Point
// =========================================================== */
class Point {
constructor(x = 0, y = 0) {
this.x = x || 0;
this.y = y || 0;
}
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
static interpolate(p1, p2, t) {
const x = p1.x * (1 - t) + p2.x * t;
const y = p1.y * (1 - t) + p2.y * t;
return new Point(x, y);
}
add(p) {
this.x += p.x;
this.y += p.y;
return this;
}
subtract(p) {
this.x -= p.x;
this.y -= p.y;
return this;
}
clone() {
return new Point(this.x, this.y);
}
}
// /* ===========================================================
// # EventTouch
// =========================================================== */
class EventTouch {
constructor() {
if (EventTouch.instance) return;
EventTouch.instance = this;
// -----------------------------
this.isDown = false; //PC用
this.currentPoint = new Point();
this.startPoint = new Point();
this.diffPoint = new Point();
this.touchesdiff = [];
this.touchesStartPoints = [new Point(), new Point()];
this.touchesCurrentPoints = [new Point(), new Point()];
this.scale = 1;
// PC
document.addEventListener("mousedown", this.onStart.bind(this), false);
document.addEventListener("mousemove", this.onMove.bind(this), false);
document.addEventListener("mouseup", this.onUp.bind(this), false);
// SP
document.addEventListener("touchstart", this.onStart.bind(this), false);
document.addEventListener("touchmove", this.onMove.bind(this), false);
document.addEventListener("touchend", this.onUp.bind(this), false);
document.addEventListener("gesturechange", this.onGestureChange.bind(this), false);
}
originalEvent(e) {
// jquery使用時用にoriginalEvent取得
return (e = e.originalEvent ? e.originalEvent : e);
}
mouseDelta(e) {
return e.deltaY ? -(e.deltaY) : e.wheelDelta ? e.wheelDelta : -(e.detail);
}
clientPosition(e) {
this.originalEvent(e);
if (e.touches) {
return new Point(e.touches[0].clientX, e.touches[0].clientY);
} else {
return new Point(e.clientX, e.clientY);
}
}
onStart(e) {
this.originalEvent(e);
this.isDown = true;
this.diffPoint = new Point();
this.startPoint = this.clientPosition(e);
if (!e.touches) return;
for (let i = e.touches.length - 1; i >= 0; i--) {
this.touchesStartPoints[i] = new Point(e.touches[i].clientX, e.touches[i].clientY);
}
}
onMove(e) {
this.originalEvent(e);
this.currentPoint = this.clientPosition(e);
if (!this.isDown) return;
this.diffPoint = new Point(this.currentPoint.x, this.currentPoint.y).subtract(this.startPoint);
if (!e.touches) return;
for (let i = e.touches.length - 1; i >= 0; i--) {
this.touchesCurrentPoints[i] = new Point(e.touches[i].clientX, e.touches[i].clientY);
}
}
onUp() {
this.isDown = false;
}
onGestureChange(e) {
this.originalEvent(e);
let scale = 1;
if (e.scale) {
scale = e.scale;
} else {
const startDistance = Point.distance(this.touchesStartPoints[0], this.touchesStartPoints[1]);
const currentDistance = Point.distance(this.touchesCurrentPoints[0], this.touchesCurrentPoints[1]);
scale = currentDistance / startDistance;
}
this.scale = scale;
}
}
// /* ===========================================================
// # Slider
// =========================================================== */
class Slider {
constructor() {
// -----------------------------
this.slider = document.querySelector(".slider");
this.slider__list = document.querySelector(".slider__list");
this.slider__main = document.querySelector(".slider__main");
this.slider__prev = document.querySelector(".slider__prev");
this.slider__next = document.querySelector(".slider__next");
this.slider__zoom = document.querySelector(".slider__zoom");
this.slider__elm = document.querySelectorAll(".slider .slider__elm");
// -----------------------------
this.state = "ready";
this.currentID = 1;
// -----------------------------
this.factor = 0.15; // イージング 係数
this.fraction = 0.001; // スライド切り替え終点位置の繰り上げ・切り下げ 端数
this.sliderWidth = 0; //スライド自体の幅
this.responseRatio = 0.1; // mouse&touchmove時にスライドorもと位置に戻すかの判定
// -----------------------------
this.currentPoint = new Point(); // 現在位置
this.targetPoint = new Point(); // 目標位置
// -----------------------------
this.resize();
this.events();
this.elements();
this.update();
}
events() {
this.slider__prev.addEventListener("click", () => {
this.slide("prev");
}, false);
this.slider__next.addEventListener("click", () => {
this.slide("next");
}, false);
this.slider__zoom.addEventListener("click", () => {
Modal.instance.open(this.currentID);
}, false);
this.slider__main.addEventListener("mousemove", this.onMove.bind(this), false);
this.slider__main.addEventListener("touchmove", this.onMove.bind(this), false);
this.slider__main.addEventListener("mouseup", this.onUp.bind(this), false);
this.slider__main.addEventListener("touchend", this.onUp.bind(this), false);
window.addEventListener("resize", this.resize.bind(this), false);
}
resize() {
this.sliderWidth = this.slider.clientWidth;
Object.assign(this.slider__main.style, {
transform: `translate(${-this.sliderWidth}px, 0px)`
});
}
onMove() {
if (this.state != "ready") return;
if (!EventTouch.instance.isDown) return;
this.targetPoint.x = EventTouch.instance.diffPoint.x;
}
onUp() {
if (this.state != "ready") return;
const ratio = Math.abs(EventTouch.instance.diffPoint.x) / this.sliderWidth;
if (ratio < this.responseRatio) {
this.targetPoint.x = 0;
return;
}
const state = (Math.sign(EventTouch.instance.diffPoint.x) != 1) ? "next" : "prev";
this.slide(state);
}
clear() {
this.currentPoint = new Point();
this.targetPoint = new Point();
this.state = "ready";
}
elements() {
const slider__elm = document.querySelectorAll(".slider .slider__elm");
const firstNode = slider__elm[0];
const lastNode = slider__elm[slider__elm.length - 1];
if (this.state == "next") {
this.slider__list.append(firstNode);
} else {
this.slider__list.insertBefore(lastNode, firstNode);
}
}
slide(_state) {
if (this.state != "ready") return;
this.state = _state;
if (_state == "prev") {
this.targetPoint.x = this.sliderWidth;
this.currentID--;
}
if (_state == "next") {
this.targetPoint.x = -this.sliderWidth;
this.currentID++;
}
if (this.currentID < 1) this.currentID = this.slider__elm.length;
if (this.currentID > this.slider__elm.length) this.currentID = 1;
}
update() {
window.requestAnimationFrame(this.update.bind(this));
this.currentPoint.x += Number(this.targetPoint.x - this.currentPoint.x) * this.factor;
const ratio = Math.abs(this.currentPoint.x) / this.sliderWidth;
if (ratio > (1.0 - this.fraction)) {
this.currentPoint.x = this.targetPoint.x;
this.elements();
this.clear();
}
if (ratio < this.fraction) {
this.currentPoint.x = this.targetPoint.x = 0;
}
Object.assign(this.slider__list.style, {
transform: `translate(${this.currentPoint.x}px, 0px)`
});
}
}
// /* ===========================================================
// # Modal
// =========================================================== */
class Modal {
constructor() {
if (Modal.instance) return;
Modal.instance = this;
// -----------------------------
this.modal = document.querySelector(".modal");
this.modal__close = document.querySelector(".modal__close");
this.delay = 500;
this.events();
}
events() {
this.modal__close.addEventListener("click", () => {
this.close();
}, false);
}
open(_id) {
const id = (`00${_id}`).slice(-2);
Array.from(document.querySelectorAll(".zoom img"), (e) => {
e.src = `https://nocebo.jp/data/zoomslider/img/${id}.jpg`;
});
this.modal.classList.add("active");
}
close() {
this.modal.classList.remove("active");
window.setTimeout(() => {
Zoom.instance.resetScale();
}, this.delay);
}
}
// /* ===========================================================
// # Timer
// =========================================================== */
class Timer {
constructor(_time) {
this.time = _time;
this.isTimeOut = false;
this.Obj = window.setTimeout(() => {
this.isTimeOut = true;
}, this.time);
}
reset() {
this.isTimeOut = false;
this.clear();
this.Obj = window.setTimeout(() => {
this.isTimeOut = true;
}, this.time);
}
clear() {
window.clearTimeout(this.Obj);
}
}
// /* ===========================================================
// # Zoom
// =========================================================== */
class Zoom {
constructor() {
if (Zoom.instance) return;
Zoom.instance = this;
// -----------------------------
this.zoom = document.querySelector(".zoom");
this.zoom__elm = document.querySelector(".zoom__elm");
this.zoom__info = document.querySelector(".zoom__info");
this.zoom__area = document.querySelector(".zoom__area");
this.zoom__origin01 = document.querySelector(".zoom__origin--01");
this.zoom__origin02 = document.querySelector(".zoom__origin--02");
// -----------------------------
this.isMouseEnter = false; // エリア内フォーカスの判定用
this.baseSize = 768; // 拡大用画像のサイズとりあえず1:1のサイズのみ
this.areaSize = this.baseSize / 5; // 拡大エリア表示のサイズ
this.currentScale = 1.0; // スケール初期値・現在値
this.scaleMin = 1.0; // スケール最小値
this.scaleMax = 3.0; // スケール最大値
this.scaleAdd = 0.02; // マウススクロールでのスケール変化値
this.mousewheelevent = "onwheel" in document ? "wheel" : "onmousewheel" in document ? "mousewheel" : "DOMMouseScroll"; //マウスホイールのイベント
this.originTimer = new Timer(100); // マウスホイールのタイマー
this.areaScale = 1.0; //表示エリアのスケール
this.factorNum = 0.10; // イージング 係数
this.fractionNum = 0.001; // 繰り上げ・切り下げ 端数
// -----------------------------
this.pointerPoint = new Point(); // マウス位置 スケール基準点を決める用
this.downPoint = new Point(); // ダウン時の要素位置
this.currentPoint = new Point(); // 現在位置
this.targetPoint = new Point(); // 目標位置
this.originPoint = new Point(0.5, 0.5); // 拡大縮小の基準点
// -----------------------------
this.resize();
this.events();
this.update();
// -----------------------------
}
normalizePoint(_x, _y) {
// elmRectとbaseSizeに合わせてマウス位置を0.0 ~ 1.0に正規化
return new Point(
this.fraction((_x - this.elmRect.left) / this.baseSize),
this.fraction((_y - this.elmRect.top) / this.baseSize)
);
}
normalizeScale() {
// スケール 値最小/最大値以内に
if (this.currentScale < this.scaleMin) this.currentScale = this.scaleMin;
if (this.currentScale > this.scaleMax) this.currentScale = this.scaleMax;
}
fraction(_v) {
// 位置端数値 繰り上げ繰り下げ
if (_v < 0.0 + this.fractionNum) _v = 0.0;
if (_v > 1.0 - this.fractionNum) _v = 1.0;
return _v;
}
resetScale() {
this.currentScale = 1.0;
this.areaScale = 1.0 / this.currentScale;
}
events() {
window.addEventListener("resize", this.resize.bind(this), false);
// PCイベント
this.zoom.addEventListener("mouseenter", () => { this.isMouseEnter = true; }, false);
this.zoom.addEventListener("mouseleave", () => { this.isMouseEnter = false; }, false);
document.addEventListener("mousedown", this.mousedown.bind(this), false);
document.addEventListener("mousemove", this.mousemove.bind(this), false);
window.addEventListener(this.mousewheelevent, this.mousewheel.bind(this), { passive: false });
// スマホイベント
document.addEventListener("touchstart", this.touchstart.bind(this), false);
document.addEventListener("touchmove", this.touchmove.bind(this), { passive: false });
document.addEventListener("gesturechange", this.gesturechange.bind(this), false);
}
resize() {
this.elmRect = this.zoom__elm.getBoundingClientRect();
}
mousedown() {
this.downPoint = new Point(this.currentPoint.x, this.currentPoint.y);
}
mousemove(e) {
if (!this.isMouseEnter) return;
this.pointerPoint = this.normalizePoint(e.clientX, e.clientY);
// マウスドラッグ時の処理
if (EventTouch.instance.isDown)
this.addDiffPoint();
}
mousewheel(e) {
// PCはマウススクロールでスケール変更
if (!this.isMouseEnter) return;
if (!e) e = window.event;
e.preventDefault();
if (EventTouch.instance.mouseDelta(e) < 0) {
this.currentScale -= this.scaleAdd;
} else {
this.currentScale += this.scaleAdd;
}
this.normalizeScale();
this.areaScale = 1.0 / this.currentScale;
if (this.originTimer.isTimeOut) this.updateOrigin();
this.originTimer.reset();
}
touchstart(e) {
e = e.originalEvent ? e.originalEvent : e;
if (e.touches.length > 1) {
const interpolate = Point.interpolate(EventTouch.instance.touchesStartPoints[0], EventTouch.instance.touchesStartPoints[1], 0.5);
this.pointerPoint = this.normalizePoint(interpolate.x, interpolate.y);
if (this.originTimer.isTimeOut) {
this.updateOrigin();
}
this.originTimer.reset();
} else {
this.downPoint = new Point(this.currentPoint.x, this.currentPoint.y);
}
}
touchmove(e) {
e.preventDefault();
if (e.touches.length > 1) return;
this.addDiffPoint();
}
gesturechange() {
this.currentScale += (EventTouch.instance.scale - 1.0) * 0.1;
this.normalizeScale();
this.areaScale = 1.0 / this.currentScale;
}
addDiffPoint() {
const addPoint = new Point(EventTouch.instance.diffPoint.x / this.baseSize, EventTouch.instance.diffPoint.y / this.baseSize);
this.targetPoint = new Point(this.downPoint.x - addPoint.x, this.downPoint.y - addPoint.y);
}
updateOrigin() {
// 拡大縮小の基準点設定
const tempOriginPoint = new Point(this.originPoint.x, this.originPoint.y);
const areaLeft = this.originPoint.x * (this.currentScale - 1.0) / this.currentScale + this.currentPoint.x * this.areaScale;
const areaTop = this.originPoint.y * (this.currentScale - 1.0) / this.currentScale + this.currentPoint.y * this.areaScale;
const originX = areaLeft + this.pointerPoint.x * this.areaScale;
const originY = areaTop + this.pointerPoint.y * this.areaScale;
this.originPoint = new Point(originX, originY);
const diffPoint = new Point(this.originPoint.x - tempOriginPoint.x, this.originPoint.y - tempOriginPoint.y);
diffPoint.x = diffPoint.x - (diffPoint.x * this.currentScale);
diffPoint.y = diffPoint.y - (diffPoint.y * this.currentScale);
this.currentPoint.x = this.targetPoint.x = (this.currentPoint.x + diffPoint.x);
this.currentPoint.y = this.targetPoint.y = (this.currentPoint.y + diffPoint.y);
}
update() {
window.requestAnimationFrame(this.update.bind(this));
this.moveArea();
this.easing();
this.addStyle();
this.updateInfo();
}
moveArea() {
// originPointに合わせてドラッグ/スワイプでの移動範囲を決める
this.areaLeftMin = this.originPoint.x - (this.originPoint.x * this.currentScale);
this.areaRightMax = -((1.0 - this.originPoint.x) - (1.0 - this.originPoint.x) * this.currentScale);
this.areaTopMin = this.originPoint.y - (this.originPoint.y * this.currentScale);
this.areaBottomMax = -((1.0 - this.originPoint.y) - (1.0 - this.originPoint.y) * this.currentScale);
}
easing() {
// 位置のイージング処理
if (this.targetPoint.x <= this.areaLeftMin)
this.targetPoint.x += (this.areaLeftMin - this.targetPoint.x) * this.factorNum;
if (this.targetPoint.x >= this.areaRightMax)
this.targetPoint.x += (this.areaRightMax - this.targetPoint.x) * this.factorNum;
if (this.targetPoint.y <= this.areaTopMin)
this.targetPoint.y += (this.areaTopMin - this.targetPoint.y) * this.factorNum;
if (this.targetPoint.y >= this.areaBottomMax)
this.targetPoint.y += (this.areaBottomMax - this.targetPoint.y) * this.factorNum;
this.currentPoint.x += (this.targetPoint.x - this.currentPoint.x) * this.factorNum;
this.currentPoint.y += (this.targetPoint.y - this.currentPoint.y) * this.factorNum;
if (Math.abs(this.currentPoint.x - this.targetPoint.x) < this.fractionNum)
this.currentPoint.x = this.targetPoint.x;
if (Math.abs(this.currentPoint.y - this.targetPoint.y) < this.fractionNum)
this.currentPoint.y = this.targetPoint.y;
}
addStyle() {
// 要素にスタイルやスケール等を反映
Object.assign(this.zoom__elm.style, {
"transform": `translate(
${-this.currentPoint.x * this.baseSize}px,
${-this.currentPoint.y * this.baseSize}px
)
scale(${this.currentScale})`,
"transform-origin": `${this.originPoint.x * this.baseSize}px ${this.originPoint.y * this.baseSize}px`
});
this.areaScale = 1.0 / this.currentScale;
Object.assign(this.zoom__area.style, {
"transform": `translate(
${this.currentPoint.x * this.areaSize * this.areaScale}px,
${this.currentPoint.y * this.areaSize * this.areaScale}px
)
scale(${this.areaScale})`,
"transform-origin": `${this.originPoint.x * this.areaSize}px ${this.originPoint.y * this.areaSize}px`
});
Object.assign(this.zoom__origin01.style, {
"transform": `translate(
${this.originPoint.x * this.baseSize}px,
${this.originPoint.y * this.baseSize}px
)`
});
Object.assign(this.zoom__origin02.style, {
"transform": `translate(
${this.originPoint.x * this.areaSize}px,
${this.originPoint.y * this.areaSize}px
)`
});
}
updateInfo() {
// 確認用テキスト更新
document.querySelector(".zoom__cp span").innerHTML = `(${this.currentPoint.x.toFixed(2)} : ${this.currentPoint.y.toFixed(2)})`;
document.querySelector(".zoom__op span").innerHTML = `(${this.originPoint.x.toFixed(2)} : ${this.originPoint.y.toFixed(2)})`;
document.querySelector(".zoom__scale span").innerHTML = `(${this.currentScale.toFixed(2)})`;
}
}
/* ===========================================================
# DOMContentLoaded
=========================================================== */
window.addEventListener("DOMContentLoaded", () => {
new EventTouch();
new Modal();
new Slider();
new Zoom();
}, false);
View Compiled
This Pen doesn't use any external JavaScript resources.