Repository: WoodNeck/css-camera
Branch: master
Commit: 94c82efbd7b4
Files: 28
Total size: 73.1 KB
Directory structure:
gitextract_yn11t5kk/
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── demo/
│ ├── css/
│ │ ├── fpv.css
│ │ ├── index.css
│ │ └── ortho.css
│ ├── fpv.html
│ ├── index.html
│ ├── js/
│ │ ├── fpv.js
│ │ ├── index.js
│ │ └── ortho.js
│ └── ortho.html
├── jsdoc.json
├── package.json
├── rollup.config.js
├── src/
│ ├── CSSCamera.ts
│ ├── constants/
│ │ ├── default.ts
│ │ └── error.ts
│ ├── index.ts
│ ├── index.umd.ts
│ ├── types.ts
│ └── utils/
│ ├── helper.ts
│ └── math.ts
├── test/
│ └── manual/
│ ├── css/
│ │ └── common.css
│ └── test.html
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/node,visualstudiocode
# Edit at https://www.gitignore.io/?templates=node,visualstudiocode
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/node,visualstudiocode
## Custom ###
.DS_Store
lib/
docs/
demo/release
demo/_data/version.yml
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 WoodNeck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 📷 CSS-CAMERA


🎥 Demo · 📄 Document
Add depth to your web page with CSS3 3D transform.
> This project is mostly inspired by [Keith Clark's work](https://keithclark.co.uk/labs/css-fps/).
## ✨ Features
- Movable, and Rotatable camera for your scene.
- Can move to in front of any element in your scene, whether it has been rotated or translated.
## ⚙️ Installation
```sh
npm i css-camera
# or
yarn add css-camera
```
## 🏃 Quick Start
```js
// Prerequisite:
// Create your scene as you like
const card = document.querySelector("#card");
const cardButton = document.querySelector("#card-button");
// First, make camera
const camera = new CSSCamera("#space");
// Call its method, then update it!
cardButton.onclick = () => {
camera.focus(card);
camera.update(2000);
}
```
Check more methods on the 📄API Documentation page
## 📜 License
[MIT](https://github.com/WoodNeck/css-camera/blob/master/LICENSE)
================================================
FILE: demo/css/fpv.css
================================================
* {
box-sizing: border-box;
transform-style: preserve-3d;
font-size: 0;
}
html, body {
position: relative;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
#space {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.zone {
width: 100%;
height: 100%;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
}
.zone0 {
left: 0;
}
.zone1 {
left: 700px;
}
.zone2 {
left: 1400px;
}
.long-hallway {
width: 200px;
height: 200px;
}
.is-left {
left: 0%;
transform-origin: left;
transform: rotateY(90deg);
}
.is-right {
right: 0%;
transform-origin: right;
transform: rotateY(-90deg);
}
.wall {
height: 200px;
position: absolute;
border: 10px solid #333;
background-color: white;
}
.is-1200 {
width: 1200px;
}
.is-1000 {
width: 1000px;
}
.is-800 {
width: 800px;
}
.is-600 {
width: 600px;
}
.is-400 {
width: 400px;
}
.is-200 {
width: 200px;
}
.arrow {
transform: rotateX(90deg);
width: 200px;
font-size: 300px;
text-align: center;
position: absolute;
bottom: 0;
transform-origin: bottom;
}
#inst {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
font-size: 72px;
}
================================================
FILE: demo/css/index.css
================================================
* {
box-sizing: border-box;
}
html, body {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
transform-style: preserve-3d;
display: flex;
justify-content: center;
}
.page-header-container {
position: absolute;
bottom: 2%;
display: flex;
justify-content: center;
flex-flow: column;
}
.page-header-wrapper {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.page-header-icon {
margin-top: auto;
margin-bottom: auto;
margin-right: 0.8rem;
}
.page-title {
color: #4a4a4a;
}
.page-header-buttons {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1%;
}
.page-header-buttons .button {
margin: 0.3rem;
}
#hero .button {
background-color: transparent;
}
#space {
display: flex;
width: 100%; height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0; padding: 0; border: 0;
transform-style: preserve-3d;
position: relative;
}
#card {
transform-style: preserve-3d;
transform: rotateX(-30deg) rotateY(45deg) translate3d(-180%, 80%, -200px);
}
.card-youtube {
width: 100%; height: 100%;
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
}
.card-side {
width: 10%; height: 100%;
position: absolute; top: 0;
background: #222;
transform-style: preserve-3d;
transform-origin: 0% 50%;
transform: rotateY(90deg);
}
#code {
max-width: 100%;
transform-style: preserve-3d;
transform: rotateZ(30deg) rotateY(-45deg) rotateX(70deg) translate3d(100%, -40%, 0px);
}
#code .title {
text-align: center;
font-family: 'Nanum Pen Script', cursive;
}
.camera-code {
padding: 1rem;
}
#hero {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
transform-style: preserve-3d;
transform: translateZ(2400px);
background-color: transparent;
}
#more {
width: 100%; min-height: 100%;
transform-style: preserve-3d;
transform: rotateX(90deg) translate3d(0, 0, -1600px);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#more h1 {
margin-top: 1.5rem;
position: absolute;
top: 0%;
}
#more .arrow {
position: absolute;
z-index: 1;
}
#more .arrow.left {
left: 1.5rem;
}
#more .arrow.right {
right: 1.5rem;
}
#demo-flicking {
width: 100%; height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.eg-flick-camera {
display: flex;
justify-content: center;
align-items: center;
}
.demo-entry {
width: 70%;
position: absolute;
z-index: 0;
display: flex;
justify-content: center;
box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.5);
}
.demo-wrapper {
width: 100%;
}
.demo-wrapper .caption-wrapper {
width: 100%; height: 100%;
display: flex;
position: absolute;
top: 0%;
left: 0%;
justify-content: center;
align-items: center;
}
.demo-wrapper .demo-caption {
font-size: 48px;
color: gold;
background-color: rgba(0, 0, 0, 0.5);
font-weight: bold;
padding: 30px;
border-radius: 10px;
}
.demo-wrapper .demo-caption:hover {
color: hotpink;
}
================================================
FILE: demo/css/ortho.css
================================================
* {
box-sizing: border-box;
}
html, body {
position: relative;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
transform-style: preserve-3d;
display: flex;
justify-content: center;
align-items: center;
}
#info {
position: fixed;
top: 12px;
color: gold;
font-size: 24px;
}
#info2 {
position: fixed;
bottom: 12px;
font-size: 24px;
}
#info2 a {
color: hotpink;
text-decoration: none;
}
#space {
transform-style: preserve-3d;
width: 640px;
height: 100%;
font-size: 0;
}
.tile {
display: inline-block;
background-size: cover;
transform-style: preserve-3d;
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
image-rendering: -webkit-optimize-contrast;/* Webkit (non-standard naming) */
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */
margin: 0; border: 0; padding: 0;
box-sizing: border-box;
width: 64px; height: 64px;
}
.tile::before {
content: '';
background-size: cover;
transform-style: preserve-3d;
position: absolute;
width: 64px; height: 32px;
transform: translateZ(-32px) scaleZ(-1) rotateZ(90deg) rotateX(-90deg);
transform-origin: 0% 0%;
}
.tile::after {
content: '';
background-size: cover;
transform-style: preserve-3d;
position: absolute;
width: 64px; height: 32px;
bottom: 0;
transform: rotateX(90deg);
transform-origin: bottom;
}
.height-1 {
transform: translateZ(32px);
}
.height-2 {
transform: translateZ(64px);
}
.height-3 {
transform: translateZ(96px);
}
.height-4 {
transform: translateZ(128px);
}
.height-5 {
transform: translateZ(160px);
}
.grass {
background-color: #8AD44E;
}
.grass::before {
background-color: #5FB535;
}
.grass::after {
background-color: #3D9756;
}
.grass.patch {
background-image: url(../asset/patch.png);
}
.grass.grassa {
background-image: url(../asset/grassa.png);
}
.grass.grassb {
background-image: url(../asset/grassb.png);
}
.grass.grassc {
background-image: url(../asset/grassc.png);
}
.grass.grassd {
background-image: url(../asset/grassd.png);
}
.grass.grasse {
background-image: url(../asset/grasse.png);
}
.grass.grassf {
background-image: url(../asset/grassf.png);
}
.grass.grassg {
background-image: url(../asset/grassg.png);
}
.grass.grassh {
background-image: url(../asset/grassh.png);
}
.grass.grassi {
background-image: url(../asset/grassi.png);
}
.grass.river-top {
background-image: url(../asset/rivertop.gif);
}
.grass.river-top-left {
background-image: url(../asset/rivertopleft.gif);
}
.grass.river-top-right {
background-image: url(../asset/rivertopleft.gif);
transform: scaleX(-1);
}
.grass.river-bottom {
background-image: url(../asset/riverbottom.gif);
}
.grass.river-bottom-left {
background-image: url(../asset/riverbottomleft.gif);
}
.grass.river-bottom-right {
background-image: url(../asset/riverbottomleft.gif);
transform: scaleX(-1);
}
.grass.road-top {
background-image: url(../asset/roadtop.png);
}
.grass.road-vertical {
background-image: url(../asset/roadmiddle.png);
}
.grass.road-bottom {
background-image: url(../asset/roadbottom.png);
}
.grass.road-left {
background-image: url(../asset/roadtop.png);
transform: rotateZ(-90deg);
}
.grass.road-horizontal {
background-image: url(../asset/roadmiddle.png);
transform: rotateZ(-90deg);
}
.grass.road-right {
background-image: url(../asset/roadbottom.png);
transform: rotateZ(-90deg);
}
.grass div {
position: absolute;
transform-origin: bottom;
background-size: cover;
}
.barrel {
width: 64px; height: 64px;
transform: translateZ(-16px) rotateX(-90deg);
background-image: url(../asset/barrel.png);
left: -25%; bottom: 50%;
}
.rock {
width: 64px; height: 64px;
transform: translateZ(-16px) rotateX(-90deg);
background-image: url(../asset/rock.png);
left: 0px; bottom: 50%;
}
.sign {
width: 64px; height: 64px;
transform: translateZ(-20px) rotateX(-90deg);
background-image: url(../asset/sign.png);
left: 0px; bottom: 50%;
}
.bush {
width: 64px; height: 64px;
transform: translateZ(-12px) rotateX(-90deg);
background-image: url(../asset/bush.gif);
left: 0px; bottom: 50%;
}
.fence {
width: 64px; height: 64px;
transform: translateZ(-18px) rotateX(-90deg);
background-image: url(../asset/fence.png);
left: 0px; bottom: 50%;
}
.tree {
width: 192px; height: 192px;
transform: rotateX(-90deg);
background-image: url(../asset/tree.gif);
left: -64px; bottom: 0px;
}
.flower {
width: 64px; height: 64px;
transform: translateZ(-18px) rotateX(-90deg);
background-image: url(../asset/flower.gif);
left: 0px; bottom: 50%;
}
.chopped {
width: 64px; height: 64px;
transform: translateZ(-2px) rotateX(-90deg);
background-image: url(../asset/chopped.gif);
left: 0px; bottom: 50%;
}
================================================
FILE: demo/fpv.html
================================================
css-camera
Click the screen to control
================================================
FILE: demo/index.html
================================================
css-camera
CSS-CAMERA
Add depth to your web page with CSS3 3D transform.
Documents
Github
Install
$ npm i css-camera
VIDEO
You can put any HTML element you like, then just transform it.
@css-camera will do the rest for you.
It's super simple!
// Prerequisite:
// Create your scene as you like
const card = document.querySelector("#card");
const cardButton = document.querySelector("#card-button");
// First, make camera
const camera = new CSSCamera("#space");
// Call its method, then update it!
cardButton.onclick = () => {
// Move camera in front of "card"
camera.focus(card);
camera.update(2000);
}
See the source of this page.
================================================
FILE: demo/js/fpv.js
================================================
const windowHeight = window.innerHeight;
const camera = new CSSCamera("#space", {
position: [0, 0, -10],
perspective: windowHeight / 2,
});
let zone = 0;
let rotate = 0;
const inst = document.querySelector("#inst");
document.documentElement.onclick = function() {
document.documentElement.requestPointerLock();
}
document.addEventListener('pointerlockchange', onLockChange, false);
document.addEventListener('mozpointerlockchange', onLockChange, false);
function onLockChange() {
if (document.pointerLockElement === document.documentElement ||
document.mozPointerLockElement === document.documentElement) {
inst.style.display = "none";
document.addEventListener("mousemove", updateMouse, false);
} else {
inst.style.display = "flex";
document.removeEventListener("mousemove", updateMouse, false);
}
}
let up = false;
let right = false;
let down = false;
let left = false;
document.addEventListener('keydown', press);
function press(e){
if (e.keyCode === 38 /* up */ || e.keyCode === 87 /* w */){
up = true;
}
if (e.keyCode === 39 /* right */ || e.keyCode === 68 /* d */){
right = true;
}
if (e.keyCode === 40 /* down */ || e.keyCode === 83 /* s */){
down = true;
}
if (e.keyCode === 37 /* left */ || e.keyCode === 65 /* a */){
left = true;
}
}
document.addEventListener('keyup', release);
function release(e){
if (e.keyCode === 38 /* up */ || e.keyCode === 87 /* w */){
up = false
}
if (e.keyCode === 39 /* right */ || e.keyCode === 68 /* d */){
right = false
}
if (e.keyCode === 40 /* down */ || e.keyCode === 83 /* s */){
down = false
}
if (e.keyCode === 37 /* left */ || e.keyCode === 65 /* a */){
left = false
}
}
const prevMouseLocation = {
x: NaN, y: NaN,
}
const clamp = function(val, min, max) {
return Math.min(Math.max(val, min), max);
}
const degToRad = function(deg) {
return Math.PI * deg / 180;
}
const clampPosition0 = function(prev, position) {
// Long Corridor
if (position[0] <= 90 || (prev[0] <= 90 && prev[2] > -1010)) {
position[0] = clamp(position[0], -90, 90);
position[2] = clamp(position[2], -1190, -10);
}
// Top
else if (prev[2] <= -1010) {
position[0] = clamp(position[0], -90, 490);
if (prev[0] > 90 && prev[0] < 310) {
position[2] = clamp(position[2], -1190, -1010);
} else {
position[2] = clamp(position[2], -1190, -10);
}
}
// Right
else if (position[0] >= 310 || (prev[0] >= 310 && prev[2] > -1010)) {
position[0] = clamp(position[0], 310, 490);
position[2] = clamp(position[2], -1190, -610);
}
return position;
}
const clampPosition1 = function(prev, position) {
position[0] = clamp(position[0], 610, 1190);
position[2] = clamp(position[2], -1190, -610);
if (prev[0] <= 790 && (prev[2] <= -790 && prev[2] > -1010)) {
position[0] = clamp(position[0], 610, 790);
}
else if (prev[2] <= -1010) {
if (prev[0] > 790 && prev[0] < 1010) {
position[2] = clamp(position[2], -1190, -1010);
} else {
position[2] = clamp(position[2], -1190, -610);
}
}
else if (prev[0] >= 1010 && (prev[2] <= -790 && prev[2] > -1010)) {
position[0] = clamp(position[0], 1010, 1190);
}
else {
if (prev[0] > 790 && prev[0] < 1010) {
position[2] = clamp(position[2], -790, -610);
} else {
position[2] = clamp(position[2], -1190, -610);
}
}
return position;
}
const clampPosition2 = function(prev, position) {
position[0] = clamp(position[0], 1310, 1890);
position[2] = clamp(position[2], -1790, -610);
// Long Corridor
if (position[0] <= 1490 || (prev[0] <= 1490 && prev[2] < -790)) {
position[0] = clamp(position[0], 1310, 1490);
position[2] = clamp(position[2], -1790, -610);
}
// Bottom
else if (prev[2] >= -790) {
if (prev[0] > 1490 && prev[0] < 1710) {
position[2] = clamp(position[2], -790, -610);
} else {
position[2] = clamp(position[2], -1790, -610);
}
}
// Right
else if (position[0] >= 310 || (prev[0] >= 310 && prev[2] > -1010)) {
position[0] = clamp(position[0], 310, 490);
position[2] = clamp(position[2], -1190, -610);
}
return position;
}
const checkZone = function(prevPos) {
const newPos = camera.position;
// Zone 0 to 1
if (zone === 0 && newPos[0] > 310 && prevPos[2] <= -980 && newPos[2] > -980) {
camera.translate(700, 0, 0);
zone = 1;
rotate = 0;
} else if (zone === 1 && newPos[0] > 1010) {
if (prevPos[2] <= -980 && newPos[2] > -980) {
rotate += 1;
} else if (prevPos[2] > -980 && newPos[2] <= -980) {
rotate -= 1;
}
if (rotate < 0) {
camera.translate(-700, 0, 0);
zone = 0;
}
} else if (zone === 1 && newPos[2] >= -790) {
if (prevPos[0] > 980 && newPos[0] <= 980) {
if (rotate > 5) {
camera.translate(700, 0, 0);
zone = 2;
}
}
} else if (zone === 2 && newPos[2] >= -790) {
if (prevPos[0] < 1680 && newPos[0] >= 1680) {
camera.translate(-700, 0, 0);
zone = 1;
}
}
}
const updateMouse = function(e) {
const diffX = e.movementX;
const diffY = e.movementY;
camera.rotate(-diffY / 5, diffX / 5);
camera.rotation = [clamp(camera.rotation[0], -85, 85), camera.rotation[1], camera.rotation[2]];
camera.update(0);
prevMouseLocation.x = e.screenX;
prevMouseLocation.y = e.screenY;
}
const speed = 5;
const keyLoop = function() {
const prevPos = camera.position.concat();
const speedVal = speed / Math.cos(degToRad(camera.rotation[0]));
if (up){
camera.translateLocal(0, 0, -speedVal);
}
if (right){
camera.translateLocal(speed, 0, 0);
}
if (down){
camera.translateLocal(0, 0, speedVal);
}
if (left){
camera.translateLocal(-speed, 0, 0);
}
var newPos = [camera.position[0], 0, camera.position[2]];
camera.position = zone === 0
? clampPosition0(prevPos, newPos)
: zone === 1
? clampPosition1(prevPos, newPos)
: clampPosition2(prevPos, newPos);
checkZone(prevPos);
camera.update(0);
requestAnimationFrame(keyLoop);
}
keyLoop();
================================================
FILE: demo/js/index.js
================================================
const windowWidth = document.body.offsetWidth;
const cardButton = document.querySelector("#card-button");
const codeButton = document.querySelector("#code-button");
const heroButton = document.querySelector("#hero-button");
const moreButton = document.querySelector("#more-button");
const buttons = [
cardButton, codeButton, heroButton, moreButton,
];
const card = document.querySelector("#card");
const code = document.querySelector("#code");
const hero = document.querySelector("#hero");
const more = document.querySelector("#more");
const restoreButtons = () => {
buttons.forEach(button => button.classList.add("is-outlined"));
}
const flicking = new eg.Flicking("#demo-flicking", {
collectStatistics: false,
adaptive: true,
zIndex: "",
gap: 50,
overflow: true
});
const camera = new CSSCamera("#space", {
perspective: windowWidth / 2,
});
camera.focus(hero);
camera.update(2000);
cardButton.onclick = () => {
camera.focus(card);
camera.update(2000);
restoreButtons();
cardButton.classList.remove("is-outlined");
}
codeButton.onclick = () => {
camera.focus(code);
camera.update(2000);
restoreButtons();
codeButton.classList.remove("is-outlined");
}
heroButton.onclick = () => {
camera.focus(hero);
camera.update(2000);
restoreButtons();
heroButton.classList.remove("is-outlined");
}
moreButton.onclick = () => {
camera.focus(more);
camera.update(2000);
restoreButtons();
moreButton.classList.remove("is-outlined");
}
================================================
FILE: demo/js/ortho.js
================================================
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const center = document.querySelector("#center");
const camera = new CSSCamera("#space");
camera.viewportEl.style.backgroundColor = "white";
camera.focus(center);
camera.update(0).then(async () => {
await camera.update(1000);
camera.rotation = [55, 0, 0];
await camera.update(1000);
camera.rotation = [55, 0, -45];
camera.scale = [2, 2, 2];
camera.viewportEl.style.backgroundColor = "black";
await camera.update(2000, {
property: "transform, background-color",
timingFunction: "ease-out, linear",
delay: "0ms, 0ms"
});
// Controls after init
const Axes = eg.Axes;
const PanInput = Axes.PanInput;
const axes = new Axes({
x: { range: [-400, 400] },
y: { range: [-150, 250] }
}, { deceleration: 0.004 }, { x: 0, y: 0 });
const panInput = new PanInput(".cc-viewport", {
scale: [0.3, 0.3],
});
axes.on({
"change": evt => {
camera.translateLocal(-evt.delta.x, -evt.delta.y);
camera.update(0);
}
});
axes.connect(["x", "y"], panInput);
});
================================================
FILE: demo/ortho.html
================================================
css-camera
================================================
FILE: jsdoc.json
================================================
{
"tags": {
"allowUnknownTags" : false,
"dictionaries": ["jsdoc", "closure"]
},
"source": {
"include": ["src", "README.md"],
"includePattern": ".+\\.(j|t)s(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"template": "node_modules/docdash",
"destination": "./docs/",
"ignores": [],
"expendsItemMembers": true,
"recurse": true
},
"plugins": [
"plugins/markdown"
],
"docdash": {
"static": true,
"cleverLinks": true,
"monospaceLinks": true,
"typedefs": true,
"private": true,
"search": true,
"openGraph": {
"title": "css-camera",
"type": "website",
"site_name": "css-camera",
"image": "",
"url": "https://woodneck.github.io/css-camera/"
},
"meta": {
"title": "css-camera",
"description": "Add a depth to your web page with CSS3 3D transform.",
"keyword": "css3, camera, graphics, 3d"
},
"menu":{
"Github":{
"href":"https://github.com/WoodNeck/css-camera",
"target":"_blank"
}
}
},
"markdown": {
"parser": "gfm",
"hardwrap": true
}
}
================================================
FILE: package.json
================================================
{
"name": "css-camera",
"version": "1.0.1-snapshot",
"description": "Add a depth to your web page with CSS3 3D transform.",
"main": "lib/css-camera.js",
"module": "lib/css-camera.esm.js",
"types": "lib/declaration/index.d.ts",
"scripts": {
"build": "rm -rf ./lib && rollup -c && npm run declaration",
"build:windows": "rd /s /q ./lib || rollup -c && npm run declaration:windows",
"declaration": "rm -rf ./lib/declaration && tsc -p tsconfig.json",
"declaration:windows": "rd /s /q ./lib || tsc -p tsconfig.json",
"demo:build": "npm run build && cpx 'lib/**/*' demo/release/latest/lib --clean",
"demo:prebuild-version": "cpx 'lib/**/*' demo/release/$npm_package_version/lib --clean && cpx 'docs/**/*' demo/release/$npm_package_version/docs --clean",
"demo:prebuild-latest": "cpx 'lib/**/*' demo/release/latest/lib --clean && cpx 'docs/**/*' demo/release/latest/docs --clean",
"demo:deploy": "npm run build && npm run doc && npm run demo:prebuild-version && npm run demo:prebuild-latest && gh-pages -d demo/",
"doc": "rm -rf ./docs && jsdoc -c jsdoc.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WoodNeck/css-camera.git"
},
"author": "WoodNeck",
"license": "MIT",
"bugs": {
"url": "https://github.com/WoodNeck/css-camera/issues"
},
"homepage": "https://github.com/WoodNeck/css-camera#readme",
"devDependencies": {
"@daybrush/jsdoc": "^0.3.7",
"@egjs/build-helper": "0.0.5",
"@types/gl-matrix": "^2.4.5",
"cpx": "^1.5.0",
"docdash": "^1.1.1",
"gh-pages": "2.0.1",
"rollup": "^1.10.1",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-prototype-minify": "^1.0.5",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-typescript": "^1.0.1",
"rollup-plugin-uglify": "^6.0.2",
"tslint": "^5.15.0",
"tslint-consistent-codestyle": "^1.15.1",
"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.4.3"
},
"dependencies": {
"gl-matrix": "^3.0.0"
}
}
================================================
FILE: rollup.config.js
================================================
const buildHelper = require("@egjs/build-helper");
const name = "CSSCamera";
const external = {
"gl-matrix": "gl-matrix",
}
export default buildHelper([
{
name,
input: "./src/index.umd.ts",
output: "./lib/css-camera.js",
format: "umd",
external,
},
{
name,
input: "./src/index.umd.ts",
output: "./lib/css-camera.min.js",
format: "umd",
uglify: true,
external,
},
{
name,
input: "./src/index.umd.ts",
output: "./lib/css-camera.pkgd.js",
format: "umd",
resolve: true,
},
{
name,
input: "./src/index.umd.ts",
output: "./lib/css-camera.pkgd.min.js",
format: "umd",
resolve: true,
uglify: true,
},
{
input: "./src/index.ts",
output: "./lib/css-camera.esm.js",
format: "esm",
external,
exports: "named",
},
]);
================================================
FILE: src/CSSCamera.ts
================================================
import { mat4, vec3, quat } from 'gl-matrix';
import { getElement, applyCSS, getTransformMatrix, findIndex, getOffsetFromParent, getRotateOffset, assign } from './utils/helper';
import { quatToEuler } from './utils/math';
import * as DEFAULT from './constants/default';
import { Offset, UpdateOption, ValueOf, Options } from './types';
class CSSCamera {
private _element: HTMLElement;
private _viewportEl: HTMLElement;
private _cameraEl: HTMLElement;
private _worldEl: HTMLElement;
private _position: vec3;
private _scale: vec3;
private _rotation: vec3;
private _perspective: number;
private _rotateOffset: number;
private _updateTimer: number;
/**
* Current version of CSSCamera.
* @example
* console.log(CSSCamera.VERSION); // ex) 1.0.0
* @type {string}
*/
static get VERSION() { return '#__VERSION__#'; }
/**
* The element provided in the constructor.
* @example
* const camera = new CSSCamera(el);
* console.log(camera.element === el); // true
* @type {HTMLElement}
*/
public get element() { return this._element; }
/**
* The reference of viewport DOM element.
* @type {HTMLElement}
*/
public get viewportEl() { return this._viewportEl; }
/**
* The reference of camera DOM element.
* @type {HTMLElement}
*/
public get cameraEl() { return this._cameraEl; }
/**
* The reference of world DOM element.
* @type {HTMLElement}
*/
public get worldEl() { return this._worldEl; }
/**
* The current position as number array([x, y, z]).
* @example
* const camera = new CSSCamera(el);
* console.log(camera.position); // [0, 0, 0];
* camera.position = [0, 0, 300];
* console.log(camera.position); // [0, 0, 300];
* @type {number[]}
*/
public get position() { return [...this._position]; }
/**
* The current scale as number array([x, y, z]).
* @example
* const camera = new CSSCamera(el);
* console.log(camera.scale); // [1, 1, 1];
* camera.scale = [5, 1, 1];
* console.log(camera.scale); // [5, 1, 1];
* @type {number[]}
*/
public get scale() { return [...this._scale]; }
/**
* The current Euler rotation angles in degree as number array([x, y, z]).
* @example
* const camera = new CSSCamera(el);
* console.log(camera.rotation); // [0, 0, 0];
* camera.rotation = [90, 0, 0];
* console.log(camera.rotation); // [90, 0, 0];
* @type {number[]}
*/
public get rotation() { return [...this._rotation]; }
/**
* The current quaternion rotation as number array([x, y, z, w]).
* @example
* const camera = new CSSCamera(el);
* console.log(camera.quaternion); // [0, 0, 0, 1];
* camera.rotation = [90, 0, 0];
* console.log(camera.quaternion); // [0.7071067690849304, 0, 0, 0.7071067690849304];
* camera.quaternion = [0, 0, 0, 1];
* console.log(camera.rotation); // [0, -0, 0];
* @type {number[]}
*/
public get quaternion() {
const r = this._rotation;
const quaternion = quat.fromEuler(quat.create(), r[0], r[1], r[2]);
return [...quaternion];
}
/**
* The current perspective value that will be applied to viewport element.
* @example
* const camera = new CSSCamera(el);
* camera.perspective = 300;
* console.log(camera.perspective); // 300
* @type {number}
*/
public get perspective() { return this._perspective; }
/**
* The current rotate offset value that will be applied to camera element.
* The camera will be as far away from the focal point as this value.
* |||
* |:---:|:---:|
* @example
* const camera = new CSSCamera(el);
* camera.perspective = 300;
* console.log(camera.cameraCSS); // scale3d(1, 1, 1) translateZ(300px) rotateX(0deg) rotateY(0deg) rotateZ(0deg);
* camera.rotateOffset = 100;
* console.log(camera.cameraCSS); // scale3d(1, 1, 1) translateZ(400px) rotateX(0deg) rotateY(0deg) rotateZ(0deg);
* @type {number}
*/
public get rotateOffset() { return this._rotateOffset; }
/**
* CSS string can be applied to camera element based on current transform.
* @example
* const camera = new CSSCamera(el);
* camera.perspective = 300;
* console.log(camera.cameraCSS); // scale3d(1, 1, 1) translateZ(300px) rotateX(0deg) rotateY(0deg) rotateZ(0deg);
* @type {string}
*/
public get cameraCSS() {
const perspective = this._perspective;
const rotateOffset = this._rotateOffset;
const rotation = this._rotation;
const scale = this._scale;
// Rotate in order of Z - Y - X
// tslint:disable-next-line: max-line-length
return `scale3d(${scale[0]}, ${scale[1]}, ${scale[2]}) translateZ(${perspective - rotateOffset}px) rotateX(${rotation[0]}deg) rotateY(${rotation[1]}deg) rotateZ(${rotation[2]}deg)`;
}
/**
* CSS string can be applied to world element based on current transform.
* ```
* const camera = new CSSCamera(el);
* console.log(camera.worldCSS); // "translate3d(0px, 0px, 0px)";
* camera.translate(0, 0, 300);
* console.log(camera.worldCSS); // "translate3d(0px, 0px, -300px)";
* ```
* @type {string}
*/
public get worldCSS() {
const position = this._position;
return `translate3d(${-position[0]}px, ${-position[1]}px, ${-position[2]}px)`;
}
public set position(val: number[]) { this._position = vec3.fromValues(val[0], val[1], val[2]); }
public set scale(val: number[]) { this._scale = vec3.fromValues(val[0], val[1], val[2]); }
public set rotation(val: number[]) { this._rotation = vec3.fromValues(val[0], val[1], val[2]); }
public set quaternion(val: number[]) { this._rotation = quatToEuler(quat.fromValues(val[0], val[1], val[2], val[3])); }
public set perspective(val: number) { this._perspective = val; }
public set rotateOffset(val: number) { this._rotateOffset = val; }
/**
* Create new CSSCamera with given element / selector.
* @param - The element to apply camera. Can be HTMLElement or CSS selector.
* @param {Partial} [options] Camera options
* @param {number[]} [options.position=[0, 0, 0]] Initial position of the camera.
* @param {number[]} [options.scale=[1, 1, 1]] Initial scale of the camera.
* @param {number[]} [options.rotation=[0, 0, 0]] Initial Euler rotation angles(x, y, z) of the camera in degree.
* @param {number} [options.perspective=0] Initial perspective of the camera.
* @param {number} [options.rotateOffset=0] Initial rotate offset of the camera.
* @example
* const camera = new CSSCamera("#el", {
* position: [0, 0, 150], // Initial pos(x, y, z)
* rotation: [90, 0, 0], // Initial rotation(x, y, z, in degree)
* perspective: 300 // CSS "perspective" value to apply
* });
*/
constructor(el: string | HTMLElement, options: Partial = {}) {
this._element = getElement(el);
const op = assign(assign({}, DEFAULT.OPTIONS), options) as Options;
this._position = vec3.fromValues(op.position[0], op.position[1], op.position[2]);
this._scale = vec3.fromValues(op.scale[0], op.scale[1], op.scale[2]);
this._rotation = vec3.fromValues(op.rotation[0], op.rotation[1], op.rotation[2]);
this._perspective = op.perspective;
this._rotateOffset = op.rotateOffset;
this._updateTimer = -1;
const element = this._element;
const viewport = document.createElement('div');
const camera = viewport.cloneNode() as HTMLElement;
const world = viewport.cloneNode() as HTMLElement;
viewport.className = DEFAULT.CLASS.VIEWPORT;
camera.className = DEFAULT.CLASS.CAMERA;
world.className = DEFAULT.CLASS.WORLD;
applyCSS(viewport, DEFAULT.STYLE.VIEWPORT);
applyCSS(camera, DEFAULT.STYLE.CAMERA);
applyCSS(world, DEFAULT.STYLE.WORLD);
camera.appendChild(world);
viewport.appendChild(camera);
this._viewportEl = viewport;
this._cameraEl = camera;
this._worldEl = world;
// EL's PARENT -> VIEWPORT -> CAMERA -> WORLD -> EL
element.parentElement!.insertBefore(viewport, element);
world.appendChild(element);
this.update(0);
}
/**
* Focus a camera to given element.
* After focus, element will be in front of camera with no rotation applied.
* Also, it will have original width / height if neither [scale](#scale) nor [perspectiveOffset](#perspectiveOffset) is applied.
* This method won't work if any of element's parent except camera element has scale applied.
* @param - The element to focus. Can be HTMLElement or CSS selector.
* @return {CSSCamera} The instance itself
*/
public focus(el: string | HTMLElement): this {
const element = getElement(el);
const focusMatrix = this._getFocusMatrix(element);
const rotation = quat.create();
const translation = vec3.create();
mat4.getRotation(rotation, focusMatrix);
mat4.getTranslation(translation, focusMatrix);
const eulerAngle = quatToEuler(rotation);
vec3.negate(eulerAngle, eulerAngle);
this._rotation = eulerAngle;
this._position = translation;
return this;
}
/**
* Translate a camera in its local coordinate space.
* For example, `camera.translateLocal(0, 0, -300)` will always move camera to direction where it's seeing.
* @param - Amount of horizontal translation, in px.
* @param - Amount of vertical translation, in px.
* @param - Amount of translation in view direction, in px.
* @return {CSSCamera} The instance itself
*/
public translateLocal(x: number = 0, y: number = 0, z: number = 0): this {
const position = this._position;
const rotation = this._rotation;
const transVec = vec3.fromValues(x, y, z);
const rotQuat = quat.create();
quat.fromEuler(rotQuat, -rotation[0], -rotation[1], -rotation[2]);
vec3.transformQuat(transVec, transVec, rotQuat);
vec3.add(position, position, transVec);
return this;
}
/**
* Translate a camera in world(absolute) coordinate space.
* @param - Amount of translation in x axis, in px.
* @param - Amount of translation in y axis, in px.
* @param - Amount of translation in z axis, in px.
* @return {CSSCamera} The instance itself
*/
public translate(x: number = 0, y: number = 0, z: number = 0): this {
vec3.add(this._position, this._position, vec3.fromValues(x, y, z));
return this;
}
/**
* Rotate a camera in world(absolute) coordinate space.
* @param - Amount of rotation in x axis, in degree.
* @param - Amount of rotation in y axis, in degree.
* @param - Amount of rotation in z axis, in degree.
* @return {CSSCamera} The instance itself
*/
public rotate(x: number = 0, y: number = 0, z: number = 0): this {
vec3.add(this._rotation, this._rotation, vec3.fromValues(x, y, z));
return this;
}
/**
* Updates a camera CSS with given duration.
* Every other camera transforming properties / methods will be batched until this method is called.
* @example
* const camera = new CSSCamera(el);
* console.log(camera.cameraEl.style.transform); // ''
*
* camera.perspective = 300;
* camera.translate(0, 0, 300);
* camera.rotate(0, 90, 0);
* console.log(camera.cameraEl.style.transform); // '', Not changed!
*
* await camera.update(1000); // Camera style is updated.
* console.log(camera.cameraEl.style.transform); // scale3d(1, 1, 1) translateZ(300px) rotateX(0deg) rotateY(90deg) rotateZ(0deg)
*
* // When if you want to apply multiple properties
* camera.update(1000, {
* property: "transform, background-color",
* timingFunction: "ease-out, ease-out", // As same with CSS, you should assign values to each property
* delay: "0ms, 100ms"
* });
* @param - Transition duration in ms.
* @param {Partial} [options] Transition options.
* @param {string} [options.property="transform"] CSS [transition-property](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-property) to apply.
* @param {string} [options.timingFunction="ease-out"] CSS [transition-timing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) to apply.
* @param {string} [options.delay="0ms"] CSS [transition-delay](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-delay) to apply.
* @return {Promise} A promise resolving instance itself
*/
public async update(duration: number = 0, options: Partial = {}): Promise {
applyCSS(this._viewportEl, { perspective: `${this.perspective}px` });
applyCSS(this._cameraEl, { transform: this.cameraCSS });
applyCSS(this._worldEl, { transform: this.worldCSS });
const updateOptions = assign(assign({}, DEFAULT.UPDATE_OPTIONS), options) as UpdateOption;
if (duration > 0) {
if (this._updateTimer > 0) {
window.clearTimeout(this._updateTimer);
}
const transitionDuration = `${duration}ms`;
const updateOption = Object.keys(updateOptions).reduce((option: {[key: string]: ValueOf}, key) => {
option[`transition${key.charAt(0).toUpperCase() + key.slice(1)}`] = updateOptions[key as keyof UpdateOption]!;
return option;
}, {});
const finalOption = {
transitionDuration,
...updateOption,
};
[this._viewportEl, this._cameraEl, this._worldEl].forEach(el => {
applyCSS(el, finalOption);
});
}
return new Promise(resolve => {
// Make sure to use requestAnimationFrame even if duration is 0
// To make sure DOM is updated, for successive update() calls.
if (duration > 0) {
this._updateTimer = window.setTimeout(() => {
// Reset transition values
[this._viewportEl, this._cameraEl, this._worldEl].forEach(el => {
applyCSS(el, { transition: '' });
});
this._updateTimer = -1;
resolve();
}, duration);
} else {
requestAnimationFrame(() => {
resolve();
});
}
});
}
private _getFocusMatrix(element: HTMLElement): mat4 {
const elements: HTMLElement[] = [];
while (element) {
elements.push(element);
if (element === this._element) break;
element = element.parentElement!;
}
// Order by shallow to deep
elements.reverse();
const elStyles = elements.map(el => window.getComputedStyle(el));
// Find first element that transform-style is not preserve-3d
// As all childs of that element is affected by its matrix
const firstFlatIndex = findIndex(elStyles, style => style.transformStyle !== 'preserve-3d');
if (firstFlatIndex > 0) { // el doesn't have to be preserve-3d'ed
elStyles.splice(firstFlatIndex + 1);
}
let parentOffset: Offset = {
left: 0,
top: 0,
width: this.viewportEl.offsetWidth,
height: this.viewportEl.offsetHeight,
};
// Accumulated rotation
const accRotation = quat.identity(quat.create());
// Assume center of screen as (0, 0, 0)
const centerPos = vec3.fromValues(0, 0, 0);
elStyles.forEach((style, idx) => {
const el = elements[idx];
const currentOffset = {
left: el.offsetLeft,
top: el.offsetTop,
width: el.offsetWidth,
height: el.offsetHeight,
};
const transformMat = getTransformMatrix(style);
const offsetFromParent = getOffsetFromParent(currentOffset, parentOffset);
vec3.transformQuat(offsetFromParent, offsetFromParent, accRotation);
vec3.add(centerPos, centerPos, offsetFromParent);
const rotateOffset = getRotateOffset(style, currentOffset);
vec3.transformQuat(rotateOffset, rotateOffset, accRotation);
const transformOrigin = vec3.clone(centerPos);
vec3.add(transformOrigin, transformOrigin, rotateOffset);
const centerFromOrigin = vec3.create();
vec3.sub(centerFromOrigin, centerPos, transformOrigin);
const invAccRotation = quat.invert(quat.create(), accRotation);
vec3.transformQuat(centerFromOrigin, centerFromOrigin, invAccRotation);
vec3.transformMat4(centerFromOrigin, centerFromOrigin, transformMat);
vec3.transformQuat(centerFromOrigin, centerFromOrigin, accRotation);
const newCenterPos = vec3.add(vec3.create(), transformOrigin, centerFromOrigin);
const rotation = mat4.getRotation(quat.create(), transformMat);
vec3.copy(centerPos, newCenterPos);
quat.mul(accRotation, accRotation, rotation);
parentOffset = currentOffset;
});
const perspective = vec3.fromValues(0, 0, this.perspective);
vec3.transformQuat(perspective, perspective, accRotation);
vec3.add(centerPos, centerPos, perspective);
const matrix = mat4.create();
mat4.fromRotationTranslation(matrix, accRotation, centerPos);
return matrix;
}
}
export default CSSCamera;
================================================
FILE: src/constants/default.ts
================================================
export const STYLE = {
VIEWPORT: {
width: '100%',
height: '100%',
'transform-style': 'preserve-3d',
overflow: 'hidden',
},
CAMERA: {
width: '100%',
height: '100%',
'transform-style': 'preserve-3d',
'will-change': 'transform',
},
WORLD: {
width: '100%',
height: '100%',
'transform-style': 'preserve-3d',
'will-change': 'transform',
},
};
export const CLASS = {
VIEWPORT: 'cc-viewport',
CAMERA: 'cc-camera',
WORLD: 'cc-world',
};
export const OPTIONS = {
position: [0, 0, 0],
scale: [1, 1, 1],
rotation: [0, 0, 0],
perspective: 0,
rotateOffset: 0,
};
export const UPDATE_OPTIONS = {
property: 'transform',
timingFunction: 'ease-out',
delay: '0ms',
};
================================================
FILE: src/constants/error.ts
================================================
export const ELEMENT_NOT_EXIST = (selector: string) => `Element with selector "${selector}" doesn't exist.`;
export const MUST_STRING_OR_ELEMENT = (received: any) => `Element should be provided in string or HTMLElement. Received: ${received}`;
================================================
FILE: src/index.ts
================================================
import CSSCamera from './CSSCamera';
export * from './types';
export default CSSCamera;
================================================
FILE: src/index.umd.ts
================================================
import CSSCamera from './CSSCamera';
export default CSSCamera;
================================================
FILE: src/types.ts
================================================
export type ValueOf = T[keyof T];
export type Matrix4x4 = [
number, number, number, number,
number, number, number, number,
number, number, number, number,
number, number, number, number,
];
/**
* @typedef
* @property - Initial position of the camera.
* @property - Initial scale of the camera.
* @property - Initial Euler rotation angles(x, y, z) of the camera in degree.
* @property - Initial perspective of the camera.
* @property - Initial rotate offset of the camera.
*/
export interface Options {
position: number[];
scale: number[];
rotation: number[];
perspective: number;
rotateOffset: number;
}
export interface Offset {
left: number;
top: number;
width: number;
height: number;
}
/**
* @typedef
* @property - CSS [transition-property](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-property) to apply.
* @property - CSS [transition-timing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) to apply.
* @property - CSS [transition-delay](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-delay) to apply.
*/
export interface UpdateOption {
property: string;
timingFunction: string;
delay: string;
}
================================================
FILE: src/utils/helper.ts
================================================
import { mat4, vec3 } from 'gl-matrix';
import { ELEMENT_NOT_EXIST, MUST_STRING_OR_ELEMENT } from '../constants/error';
import { Matrix4x4, Offset } from '../types';
export const getElement = (el: string | HTMLElement, baseElement?: HTMLElement): HTMLElement => {
if (typeof el === 'string') {
const queryResult = baseElement
? baseElement.querySelector(el)
: document.querySelector(el);
if (!queryResult) {
throw new Error(ELEMENT_NOT_EXIST(el));
}
return queryResult as HTMLElement;
} else if (el.nodeName && el.nodeType === 1) {
return el;
} else {
throw new Error(MUST_STRING_OR_ELEMENT(el));
}
};
export function applyCSS(element: HTMLElement, cssObj: { [keys: string]: string }): void {
Object.keys(cssObj).forEach(property => {
(element.style as any)[property] = cssObj[property];
});
}
export function getTransformMatrix(elStyle: CSSStyleDeclaration): mat4 {
const trVal = elStyle.getPropertyValue('transform');
const transformStr = /\(((\s|\S)+)\)/.exec(trVal);
const matrixVal = transformStr
? transformStr[1].split(',').map(val => parseFloat(val)) as Matrix4x4
: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] as Matrix4x4;
if (matrixVal.length === 16 ) {
return mat4.fromValues(...matrixVal);
} else {
// Convert 2d matrix(length 6) to 3d
const matrix = mat4.create();
mat4.identity(matrix);
matrix[0] = matrixVal[0];
matrix[1] = matrixVal[1];
matrix[4] = matrixVal[2];
matrix[5] = matrixVal[3];
matrix[12] = matrixVal[4];
matrix[13] = matrixVal[5];
return matrix;
}
}
export function getOffsetFromParent(currentOffset: Offset, parentOffset: Offset): vec3 {
const offsetLeft = currentOffset.left + (currentOffset.width - parentOffset.width) / 2;
const offsetTop = currentOffset.top + (currentOffset.height - parentOffset.height) / 2;
return vec3.fromValues(offsetLeft, offsetTop, 0);
}
export function getRotateOffset(elStyle: CSSStyleDeclaration, currentOffset: Offset): vec3 {
const axis = (elStyle.transformOrigin as string)
.split(' ')
.map(str => parseFloat(str.substring(0, str.length - 2)));
const ax = axis[0] - currentOffset.width / 2;
const ay = axis[1] - currentOffset.height / 2;
return vec3.fromValues(ax, ay, 0);
}
export function findIndex(iterable: T[], callback: (el: T) => boolean): number {
for (let i = 0; i < iterable.length; i += 1) {
const element = iterable[i];
if (element && callback(element)) {
return i;
}
}
return -1;
}
// return [0, 1, ...., max - 1]
export function range(max: number): number[] {
const counterArray: number[] = [];
for (let i = 0; i < max; i += 1) {
counterArray[i] = i;
}
return counterArray;
}
export function clamp(val: number, min: number, max: number): number {
return Math.max(Math.min(val, max), min);
}
export function assign(target: object, ...srcs: object[]): object {
srcs.forEach(source => {
Object.keys(source).forEach(key => {
const value = (source as any)[key];
(target as any)[key] = value;
});
});
return target;
}
================================================
FILE: src/utils/math.ts
================================================
import { mat4, quat, vec3 } from 'gl-matrix';
import { clamp } from './helper';
export function degToRad(deg: number): number {
return Math.PI * deg / 180;
}
export function radToDeg(rad: number): number {
return 180 * rad / Math.PI;
}
// From Three.js https://github.com/mrdoob/three.js/blob/dev/src/math/Euler.js
export function quatToEuler(q: quat): vec3 {
const rotM = mat4.create();
mat4.fromQuat(rotM, q);
const m11 = rotM[0];
const m12 = rotM[4];
// const m13 = rotM[8];
const m21 = rotM[1];
const m22 = rotM[5];
// const m23 = rotM[9];
const m31 = rotM[2];
const m32 = rotM[6];
const m33 = rotM[10];
const euler = vec3.create();
// ZYX
euler[1] = Math.asin(-clamp(m31, -1, 1));
if (Math.abs(m31) < 0.99999) {
euler[0] = Math.atan2(m32, m33);
euler[2] = Math.atan2(m21, m11);
} else {
euler[0] = 0;
euler[2] = Math.atan2(-m12, m22);
}
return euler.map(val => radToDeg(val)) as vec3;
}
================================================
FILE: test/manual/css/common.css
================================================
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
min-height: 100%;
margin: 0;
}
/* .cc-camera {
transition: transform 1s;
}
.cc-world {
transition: transform 1s;
} */
#space {
transform-style: preserve-3d;
position: relative;
width: 100%;
height: 100%;
}
.cube {
display: inline-block;
width: 300px;
height: 300px;
position: absolute;
left: calc(50% - 150px);
top: calc(50% - 150px);
transform-style: preserve-3d;
transition: transform 3s;
}
#cube2 {
transform: translate3d(150px, 0, -150px) rotateY(-90deg);
}
#cube3 {
transform: translate3d(300px, 0, 0px) rotateY(180deg);
}
.cube-side {
width: 100%;
height: 100%;
background: rgb(114, 55, 55);
position: absolute;
left: 0; top: 0;
}
.cube-side.up {
background: #222;
transform-origin: top;
transform: rotateX(-90deg);
}
.cube-side.down {
background: #444;
transform-origin: bottom;
transform: rotateX(90deg);
}
.cube-side.left {
background: #666;
transform-origin: left;
transform: rotateY(90deg);
}
.cube-side.right {
background: #888;
transform-origin: right;
transform: rotateY(-90deg);
}
#cube1 .cube-side.right,
#cube2 .cube-side.right {
background: transparent;
}
.cube-side.front {
background: url("../assets/cover.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.cube-side.back {
background: url(https://thumbs.gfycat.com/IdealisticOilyFlicker-size_restricted.gif);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
transform: translateZ(-300px);
transform-style: preserve-3d;
}
#rotated {
transform-style: preserve-3d;
transform: rotateZ(-45deg) translate3d(200px, -500px, 300px) rotateX(90deg);
/* transform-origin: 50px 30%; */
width: 50vw;
height: 50vh;
transform-origin: 0% 0%;
background: aquamarine;
backface-visibility: hidden;
}
#rotated2 {
transform-style: preserve-3d;
transform: rotateY(15deg) translate3d(200px, -500px, 300px) rotateZ(10deg);
transform-origin: 0% 50px;
width: 50vw;
height: 50vh;
background: chartreuse;
backface-visibility: hidden;
}
#rotated3 {
transform-style: preserve-3d;
transform: translate3d(200px, -500px, 300px) rotateX(30deg) rotateY(-30deg);
transform-origin: 0% 120px;
width: 50vw;
height: 50vh;
background: tomato;
backface-visibility: hidden;
}
================================================
FILE: test/manual/test.html
================================================
CSS-Camera Demo Page
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
// Module
"module": "es2015",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// Path
"rootDir": "./src",
"outDir": "./lib/",
// Delcaration
"declaration": true,
"declarationDir": "./lib/declaration",
// Log
"pretty": true,
// Lint
"strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"newLine": "lf",
// Outfile
"charset": "utf8",
"removeComments": true,
"noEmitOnError": true,
// SourceMap
"sourceMap": true,
// etc
"importHelpers": true,
"downlevelIteration": true
},
"include": [
"./src/**/*.ts"
]
}
================================================
FILE: tslint.json
================================================
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"rulesDirectory": [
"tslint-consistent-codestyle",
"tslint-eslint-rules"
],
"rules": {
"quotemark": [true, "single"],
"variable-name": false,
"arrow-parens": false,
"object-literal-key-quotes": false,
"object-literal-sort-keys": false,
"object-literal-shorthand": false,
"ordered-imports": false,
"no-console": false,
"curly": false,
"adjacent-overload-signatures": false,
"no-bitwise": false,
"interface-name": false,
"max-line-length": false
},
"linterOptions": {
"exclude": [
"node_modules/**/*"
]
}
}