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 ![npm](https://img.shields.io/npm/v/css-camera?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/woodneck/css-camera?style=for-the-badge)

🎥 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
Placeholder image

WoodNeck

@woodneck

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.

More demos?

🎥 Main Card Code More
================================================ 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
All pixelart assets are from https://guttykreum.itch.io/field-of-green
Source
================================================ 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. * |![rot0](https://woodneck.github.io/css-camera/asset/rot0.gif)|![rot150](https://woodneck.github.io/css-camera/asset/rot150.gif)| * |:---:|:---:| * @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
TEXT
TEXT
TEXT
˜
================================================ 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/**/*" ] } }