Repository: petargyurov/virtual-bookshelf
Branch: main
Commit: eedea4cc5d23
Files: 6
Total size: 10.4 KB
Directory structure:
gitextract_4f_gryi5/
├── LICENSE
├── README.md
├── bookshelf.css
├── bookshelf.js
├── cssUtils.js
└── index.html
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <https://unlicense.org>
================================================
FILE: README.md
================================================
# Virtual Bookshelf
This is a simple visualisation of books on a bookshelf using some CSS transforms to give the effect of picking out the book when you hover over it.
I use it on my [personal site](https://petargyurov.com) to track what books I've read. It integrates nicely with static site generators, and well, just about anything since it's all vanilla JS, CSS and HTML.

**How do I add more books?**
A book is defined as follows:
```html
<div class="book">
<div class="side spine">
<span class="spine-title"> Book Title </span>
<span class="spine-author"> PG </span>
</div>
<div class="side top"></div>
<div class="side cover"></div>
</div>
```
Simply add this snippet for each book you want inside `<div class="bookshelf">`.
**Why use JS at all?**
I originally aimed for a no-JS implementation but there was no way of adding randomness to the book height, colours and patterns without it. You can of course remove the JS and implement that stuff manually.
**Is this free to use?**
Yep, do whatever you want with it.
If you found this project useful you can make use of the following badge to spread the word:
[](https://github.com/petargyurov/virtual-bookshelf)
**Is it perfect?**
Nope. Doesn't handle long titles well. I'm sure there are other alignment issues. I wrote this in a day, don't expect much. Feel free to submit fixes/improvements.
================================================
FILE: bookshelf.css
================================================
:root {
--spine-pyramid: linear-gradient(
315deg,
transparent 75%,
rgba(255, 255, 255, 0.1) 0
),
linear-gradient(
45deg,
transparent 75%,
rgba(255, 255, 255, 0.1) 0
),
linear-gradient(
135deg,
rgba(255, 255, 255, 0.2) 166px,
transparent 0
),
linear-gradient(
45deg,
rgba(0, 0, 0, 0.1) 75%,
transparent 0
);
background-size: 20px 20px;
--spine-stairs: repeating-linear-gradient(
63deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.1) 1px,
transparent 3px,
transparent 0
),
linear-gradient(
127deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.1) 90px,
transparent 55%,
transparent 0
),
linear-gradient(
transparent 51%,
rgba(0, 0, 0, 0.1) 170px
);
background-size: 70px 120px;
--spine-argyle: repeating-linear-gradient(
120deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.1) 1px,
transparent 1px,
transparent 60px
),
repeating-linear-gradient(
60deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.1) 1px,
transparent 1px,
transparent 60px
),
linear-gradient(
60deg,
rgba(0, 0, 0, 0.1) 25%,
transparent 25%,
transparent 75%,
rgba(0, 0, 0, 0.1) 75%,
rgba(0, 0, 0, 0.1)
),
linear-gradient(
120deg,
rgba(0, 0, 0, 0.1) 25%,
transparent 25%,
transparent 75%,
rgba(0, 0, 0, 0.1) 75%,
rgba(0, 0, 0, 0.1)
);
background-size: 70px 120px;
--spine-tartan: repeating-linear-gradient(
transparent,
transparent 50px,
rgba(0, 0, 0, 0.4) 50px,
rgba(0, 0, 0, 0.4) 53px,
transparent 53px,
transparent 63px,
rgba(0, 0, 0, 0.4) 63px,
rgba(0, 0, 0, 0.4) 66px,
transparent 66px,
transparent 116px,
rgba(0, 0, 0, 0.5) 116px,
rgba(0, 0, 0, 0.5) 166px,
rgba(255, 255, 255, 0.2) 166px,
rgba(255, 255, 255, 0.2) 169px,
rgba(0, 0, 0, 0.5) 169px,
rgba(0, 0, 0, 0.5) 179px,
rgba(255, 255, 255, 0.2) 179px,
rgba(255, 255, 255, 0.2) 182px,
rgba(0, 0, 0, 0.5) 182px,
rgba(0, 0, 0, 0.5) 232px,
transparent 232px
),
repeating-linear-gradient(
270deg,
transparent,
transparent 50px,
rgba(0, 0, 0, 0.4) 50px,
rgba(0, 0, 0, 0.4) 53px,
transparent 53px,
transparent 63px,
rgba(0, 0, 0, 0.4) 63px,
rgba(0, 0, 0, 0.4) 66px,
transparent 66px,
transparent 116px,
rgba(0, 0, 0, 0.5) 116px,
rgba(0, 0, 0, 0.5) 166px,
rgba(255, 255, 255, 0.2) 166px,
rgba(255, 255, 255, 0.2) 169px,
rgba(0, 0, 0, 0.5) 169px,
rgba(0, 0, 0, 0.5) 179px,
rgba(255, 255, 255, 0.2) 179px,
rgba(255, 255, 255, 0.2) 182px,
rgba(0, 0, 0, 0.5) 182px,
rgba(0, 0, 0, 0.5) 232px,
transparent 232px
),
repeating-linear-gradient(
125deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.2) 2px,
rgba(0, 0, 0, 0.2) 3px,
transparent 3px,
transparent 5px,
rgba(0, 0, 0, 0.2) 5px
);
}
.bookshelf {
width: 100%;
margin-top: 32px;
display: flex;
flex-wrap: wrap;
}
.book {
width: 50px;
height: 280px;
position: relative;
margin-left: 1px;
transform-style: preserve-3d;
transform: translateZ(0) rotateY(0);
transition: transform 1s;
}
.side {
position: absolute;
border: 2px solid black;
border-radius: 3px;
font-weight: bold;
color: black;
text-align: center;
transform-origin: center left;
}
.spine {
position: relative;
width: 50px;
height: 280px;
/* Patterns from: https://projects.verou.me/css3patterns/ */
background-image: var(--tartan);
transform: rotateY(0deg) translateZ(0px);
}
.spine-title {
margin: 2px;
position: absolute;
top: 0px;
left: 0px;
font-size: 12px;
color: gold;
writing-mode: vertical-rl;
text-orientation: mixed;
}
.spine-author {
position: absolute;
color: goldenrod;
bottom: 0px;
left: 20%; /* no idea why 20% centers it */
}
.top {
width: 50px;
height: 190px;
top: -2px; /* hmm, why -2 and not 0? */
background-image: linear-gradient(90deg, white 90%, gray 10%);
background-size: 5px 5px;
transform: rotateX(90deg) translateZ(95px) translateY(-95px);
}
.cover {
width: 190px;
height: 280px;
top: 0px;
background-image: url("https://picsum.photos/190/280");
background-size: contain;
background-repeat: round;
left: 50px;
transform: rotateY(90deg) translateZ(0);
transition: transform 1s;
}
.book:hover {
z-index: 1;
transform: rotateX(-25deg) rotateY(-40deg) rotateZ(-15deg) translateY(50px)
translateX(-30px);
}
================================================
FILE: bookshelf.js
================================================
import { getRootCssStyles} from './cssUtils.js';
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
}
let spines = Object.values(document.getElementsByClassName("spine"));
let covers = Object.values(document.getElementsByClassName("cover"));
let tops = Object.values(document.getElementsByClassName("top"));
let availablePatterns = getRootCssStyles();
let availableColors = [
"maroon",
"darkgreen",
"darkolivegreen",
"brown",
"saddlebrown",
"sienna",
"midnightblue",
];
// assign a random height, pattern and colour to each book
spines.map(function (s, i) {
let randomHeight = getRandomInt(220, 290);
s.style.height = `${randomHeight}px`;
s.style.top = `${280 - randomHeight}px`;
let randomPattern = randomChoice(availablePatterns);
s.style.backgroundImage = `var(${randomPattern})`;
let randomColor = randomChoice(availableColors);
s.style.backgroundColor = randomColor;
covers[i].style.height = `${randomHeight}px`;
covers[i].style.top = `${280 - randomHeight}px`;
tops[i].style.top = `${280 - randomHeight}px`;
});
================================================
FILE: cssUtils.js
================================================
export function getRootCssStyles(rootRule = ":root") {
// Get all CSS rules for the document using Array methods
const cssRulesArray = [...document.styleSheets]
.map(styleSheet => {
try {
return [...styleSheet.cssRules]
.map(rule => rule)
} catch (e) {
// console.log('Access to stylesheet %s is denied. Ignoring...', styleSheet.href);
}
})
var cssVars = [];
// Get custom styles from root css rule
Object.values(cssRulesArray).forEach(arrayElement => {
Object.values(arrayElement).forEach(ruleElement => {
if (ruleElement.selectorText === rootRule) {
Object.values(ruleElement.style).forEach(style => {
if (style.startsWith('--spine-') && cssVars.indexOf(style) == -1) {
cssVars.push(style);
}
})
}
})
})
return cssVars;
}
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="bookshelf.css" />
</head>
<body>
<div class="bookshelf">
<div class="book">
<div class="side spine">
<span class="spine-title"> Book Title </span>
<span class="spine-author"> PG </span>
</div>
<div class="side top"></div>
<div class="side cover"></div>
</div>
</div>
</body>
<script type="module" src="bookshelf.js"></script>
</html>
gitextract_4f_gryi5/ ├── LICENSE ├── README.md ├── bookshelf.css ├── bookshelf.js ├── cssUtils.js └── index.html
SYMBOL INDEX (3 symbols across 2 files)
FILE: bookshelf.js
function getRandomInt (line 3) | function getRandomInt(min, max) {
function randomChoice (line 9) | function randomChoice(array) {
FILE: cssUtils.js
function getRootCssStyles (line 1) | function getRootCssStyles(rootRule = ":root") {
Condensed preview — 6 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (11K chars).
[
{
"path": "LICENSE",
"chars": 1211,
"preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
},
{
"path": "README.md",
"chars": 1550,
"preview": "# Virtual Bookshelf\nThis is a simple visualisation of books on a bookshelf using some CSS transforms to give the effect "
},
{
"path": "bookshelf.css",
"chars": 5200,
"preview": ":root {\n --spine-pyramid: linear-gradient(\n 315deg,\n transparent 75%,\n rgba(255, 255, 255, 0.1) 0\n "
},
{
"path": "bookshelf.js",
"chars": 1245,
"preview": "import { getRootCssStyles} from './cssUtils.js';\n\nfunction getRandomInt(min, max) {\n min = Math.ceil(min);\n max = Math"
},
{
"path": "cssUtils.js",
"chars": 995,
"preview": "export function getRootCssStyles(rootRule = \":root\") {\n // Get all CSS rules for the document using Array methods\n "
},
{
"path": "index.html",
"chars": 490,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <link rel=\"stylesheet\" href=\"bookshelf.css\" />\n </head>\n\n <body>\n <di"
}
]
About this extraction
This page contains the full source code of the petargyurov/virtual-bookshelf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 6 files (10.4 KB), approximately 3.4k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.