diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..f8414f0 --- /dev/null +++ b/404.html @@ -0,0 +1,3 @@ + +404 Not Found +

404 Not Found

diff --git a/drag-and-drop-in-vr-with-lovr/index.html b/drag-and-drop-in-vr-with-lovr/index.html new file mode 100644 index 0000000..51f0b86 --- /dev/null +++ b/drag-and-drop-in-vr-with-lovr/index.html @@ -0,0 +1,600 @@ + + + + + + + micouy's blog + + + +
+home + +

+ Drag & drop in VR with LÖVR +

+ +

2024-11-29

+ +

LÖVR is a delightful to use VR framework. In this post I'll explain how to implement a simple drag & drog feature in it.

+

For LÖVR beginners, here's a guide on how to get started.

+

Surprisingly you don't need a headset to run LÖVR apps - you can just call lovr . in a folder with a main.lua file. This will launch an emulator on your computer. For our drag & drop feature however, we will need the headset to get the positions, orientations and gestures of our hands. I'm using a Meta Quest 3. If you don't have a headset yet, I'd recommend checking out Meta Quest 3S, because it's so cheap.

+

Hello World & Launching the app on your headset

+

We'll start with a simple hello world app:

+
function lovr.draw(pass)
+    pass:text("hello world", vec3(0, 1, -1), 0.1, quat())
+end
+
+ +

vec3(0, 1, -1) the position of the text, where negative Z axis is in the direction the headset is facing.

+

0.1 is the scale of the text.

+

quat constructs a default quaternion, a mathematical object storing the information about the orientation in three dimensions.

+
+

That's the whole program. You can save it to a file named main.lua in a new project folder. To launch it on your headset, follow these instructions.

+

Hand tracking with LÖVR

+

LÖVR exposes a very simple API for tracking both hands and controllers. The whole API is documented very well in the docs, together with examples.

+

We can start by copying the Tracked Hands example:

+
function lovr.draw(pass)
+  for i, hand in ipairs(lovr.headset.getHands()) do
+    local x, y, z = lovr.headset.getPosition(hand)
+    pass:sphere(x, y, z, .1)
+  end
+end
+
+ +If you have trouble with hand tracking, you can switch to controllers without changing the code. + +

After launching it on your headset, you'll see two white spheres following your hands positions.

+

To study what exactly the code does, you can check out these pages in the docs:

+ +

User input

+

Now we need to detect grab events. We'll use the trigger input fired when the controller trigger is pressed or when the user pinches their fingers.

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local x, y, z = lovr.headset.getPosition(hand)
+
+        -- Set white color by default.
+        pass:setColor(0xffffff)
+
+        -- Set red color if the user just tried to grab.
+        if lovr.headset.wasPressed(hand,'trigger') then
+            pass:setColor(0xff0000)
+        end
+
+        pass:sphere(x, y, z, .1)
+    end
+end
+
+

Now whenever you pinch your fingers, you'll see the spheres flicker red for a single frame.

+

Let's display something that we can grab.

+
local box1 = {
+    position = lovr.math.newVec3(0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.3, 0.3, 0.3),
+}
+
+local box2 = {
+    position = lovr.math.newVec3(-0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.5, 0.5, 0.5),
+}
+
+local boxes = { box1, box2 }
+
+function lovr.draw(pass)
+    -- The rest of the owl...
+
+    for _, box in ipairs(boxes) do
+        pass:box(box.position, box.dimensions, quat(), 'line')
+    end
+end
+
+

See Pass:box.

+
+ +A note on vec3(...) and lovr.math.newVec3(...) + +

You might notice that in the Hello World example I instantiated a vector using vec3(...) and here I used lovr.math.newVec3(...). It's not random.

+

If the vector is used only within one frame (it isn't assigned to a variable used outside of lovr.draw or lovr.update), I can construct a temporary vector with vec3(...). If I need a vector to live for longer, I need to use lovr.math.newVec3(...).

+

If you use a temporary vector during another frame, you'll get an error saying 'Attempt to use a temporary vector from a previous frame'.

+
+

Now we need to detect whether the hand was touching the cube while the grab event fired.

+

We'll add an isActive property to each box...

+
local box1 = {
+    position = lovr.math.newVec3(0.5, 1.5, -0.5),
+    dimensions = lovr.math.newVec3(0.3, 0.3, 0.3),
+    isActive = false,
+}
+
+local box2 = {
+    position = lovr.math.newVec3(-0.5, 1.5, -0.5),
+    dimensions = lovr.math.newVec3(0.5, 0.5, 0.5),
+    isActive = false,
+}
+
+

...convert the coordinates returned by getPosition(...) into a vector and update code which displays the spheres...

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        pass:setColor(0xffffff)
+        pass:sphere(handPosition, .1)
+    end
+end
+
+

...then toggle isActive of a box the user grabbed...

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        for _, box in ipairs(boxes) do
+            if wasPressed and isPointInsideBox(handPosition, box) then
+                box.isActive = not box.isActive
+            end
+        end
+
+        pass:setColor(0xffffff)
+        pass:sphere(handPosition, .1)
+    end
+end
+
+

We'll also change box's color depending on whether it is active:

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        -- ...
+    end
+
+    for _, box in ipairs(boxes) do
+        if box.isActive then
+            pass:setColor(0x00ff00)
+        else
+            pass:setColor(0xffffff)
+        end
+
+        pass:box(box.position, box.dimensions, quat(), 'line')
+    end
+end
+
+

And lastly, let's implement the isPointInsideBox function:

+
function isPointInsideBox(point, box)
+    local relativePoint = point - box.position + (box.dimensions / 2)
+    local width, height, depth = box.dimensions:unpack()
+
+    return (
+        relativePoint.x > 0 and relativePoint.x < width
+        and relativePoint.y > 0 and relativePoint.y < height
+        and relativePoint.z > 0 and relativePoint.z < depth
+    )
+end
+
+

We're adding box.dimensions / 2 because box.position marks the center of the drawn box, not its corner.

+
+ +Whole code + +
local box1 = {
+    position = lovr.math.newVec3(0.5, 1.5, -0.5),
+    dimensions = lovr.math.newVec3(0.3, 0.3, 0.3),
+    isActive = false,
+}
+
+local box2 = {
+    position = lovr.math.newVec3(-0.5, 1.5, -0.5),
+    dimensions = lovr.math.newVec3(0.5, 0.5, 0.5),
+    isActive = false,
+}
+
+local boxes = { box1, box2 }
+
+function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        for _, box in ipairs(boxes) do
+            if wasPressed and isPointInsideBox(handPosition, box) then
+                box.isActive = not box.isActive
+            end
+        end
+
+        pass:setColor(0xffffff)
+        pass:sphere(handPosition, .1)
+    end
+
+    for _, box in ipairs(boxes) do
+        if box.isActive then
+            pass:setColor(0x00ff00)
+        else
+            pass:setColor(0xffffff)
+        end
+
+        pass:box(box.position, box.dimensions, quat(), 'line')
+    end
+end
+
+function isPointInsideBox(point, box)
+    local relativePoint = point - box.position + (box.dimensions / 2)
+    local width, height, depth = box.dimensions:unpack()
+
+    return (
+        relativePoint.x > 0 and relativePoint.x < width
+        and relativePoint.y > 0 and relativePoint.y < height
+        and relativePoint.z > 0 and relativePoint.z < depth
+    )
+end
+
+
+

If you run your code now, you should see the boxes change its color to green when you pinch them, and then back to white when you do it again.

+

Grabbing

+

Now that we're able to detect whether the user grabbed something, let's make it follow our hand.

+

We need to keep track of which hand is grabbing which box in order to move the boxes correctly. We'll store which hand is grabbing which box together with the offsets:

+
local box1 = {
+    -- ...
+}
+
+local box2 = {
+    -- ...
+}
+
+local boxes = { box1, box2 }
+
+local grabbedBoxes = {
+    ["hand/left"] = nil,
+    ["hand/right"] = nil,
+}
+
+ +"hand/left" and "hand/right" are the hand identifiers used by LÖVR. + +

On each trigger event we'll iterate over each box and store it in grabbedBoxes if it was grabbed, alongside the offset...

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        if wasPressed then
+            for _, box in ipairs(boxes) do
+                if isPointInsideBox(handPosition, box) then
+                    grabbedBoxes[hand] = {
+                        box = box,
+                        offset = lovr.math.newVec3(handPosition - box.position),
+                    }
+                end
+            end
+        end
+
+        -- ...
+    end
+
+    -- ...
+end
+
+

...or remove it from grabbedBoxes if it was released...

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        if wasPressed then
+            -- ...
+        end
+
+        local wasReleased = lovr.headset.wasReleased(hand, 'trigger')
+
+        if wasReleased then
+            grabbedBoxes[hand] = nil
+        end
+
+        -- ...
+    end
+
+    -- ...
+end
+
+

...and lastly move the grabbed box:

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        if wasPressed then
+            -- ...
+        end
+
+        local wasReleased = lovr.headset.wasReleased(hand, 'trigger')
+
+        if wasReleased then
+            grabbedBoxes[hand] = nil
+        end
+
+        local grabbedBox = grabbedBoxes[hand]
+
+        if grabbedBox ~= nil then
+            grabbedBox.box.position:set(handPosition - grabbedBox.offset)
+        end
+
+        pass:sphere(handPosition, .1)
+    end
+
+    -- ...
+end
+
+

To highlight a grabbed box, we'll change its color:

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        -- ...
+    end
+
+    for _, box in ipairs(boxes) do
+        local isGrabbed = (
+            (grabbedBoxes.left and grabbedBoxes.left.box == box)
+            or (grabbedBoxes.right and grabbedBoxes.right.box == box)
+        )
+
+        if isGrabbed then
+            pass:setColor(0x00ff00)
+        else
+            pass:setColor(0xffffff)
+        end
+
+        pass:box(box.position, box.dimensions, quat(), 'line')
+    end
+end
+
+
+ +Whole code + +
local box1 = {
+    position = lovr.math.newVec3(0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.3, 0.3, 0.3),
+}
+
+local box2 = {
+    position = lovr.math.newVec3(-0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.5, 0.5, 0.5),
+}
+
+local boxes = { box1, box2 }
+
+local grabbedBoxes = {
+    ["hand/left"] = nil,
+    ["hand/right"] = nil,
+}
+
+function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        if wasPressed then
+            for _, box in ipairs(boxes) do
+                if isPointInsideBox(handPosition, box) then
+                    grabbedBoxes[hand] = {
+                        box = box,
+                        offset = lovr.math.newVec3(handPosition - box.position),
+                    }
+                end
+            end
+        end
+
+        local wasReleased = lovr.headset.wasReleased(hand, 'trigger')
+
+        if wasReleased then
+            grabbedBoxes[hand] = nil
+        end
+
+        local grabbedBox = grabbedBoxes[hand]
+
+        if grabbedBox ~= nil then
+            grabbedBox.box.position:set(handPosition - grabbedBox.offset)
+        end
+
+        pass:sphere(handPosition, .1)
+    end
+
+    for _, box in ipairs(boxes) do
+        local isGrabbed = (
+            (grabbedBoxes["hand/left"] ~= nil and grabbedBoxes["hand/left"].box == box)
+            or (grabbedBoxes["hand/right"] ~= nil and grabbedBoxes["hand/right"].box == box)
+        )
+
+        if isGrabbed then
+            pass:setColor(0x00ff00)
+        else
+            pass:setColor(0xffffff)
+        end
+
+        pass:box(box.position, box.dimensions, quat(), 'line')
+    end
+end
+
+function isPointInsideBox(point, box)
+    local relativePoint = point - box.position + (box.dimensions / 2)
+    local width, height, depth = box.dimensions:unpack()
+
+    return (
+        relativePoint.x > 0 and relativePoint.x < width
+        and relativePoint.y > 0 and relativePoint.y < height
+        and relativePoint.z > 0 and relativePoint.z < depth
+    )
+end
+
+
+

Now you should have a working drag'n' drop feature. But we can extend it a bit.

+

Rotating boxes

+

Rotation works analogously to translation – we need to store the "offset" of orientations at the moment of grabbing in order to apply it in each frame relative to hands current orientation.

+

First, store the orientation of each box:

+
local box1 = {
+    -- ...
+    orientation = lovr.math.newQuat(),
+}
+
+local box2 = {
+    -- ...
+    orientation = lovr.math.newQuat(),
+}
+
+

Fetch hand orientation:

+
function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+        local handOrientation = quat(lovr.headset.getOrientation(hand))
+
+        -- ...
+    end
+
+    -- ...
+end
+
+

We'll modify entries into grabbedBoxes:

+
positionOffset = quat(handOrientation):conjugate() * (handPosition - box.position)
+orientationOffset = quat(handOrientation):conjugate() * box.orientation
+
+grabbedBoxes[hand] = {
+    box = box,
+    positionOffset = lovr.math.newVec3(positionOffset),
+    orientationOffset = lovr.math.newQuat(orientationOffset),
+}
+
+

When multiplying a quaternion's conjugate by another quaternion (box.orientation) or by a vector (box.position), you get an offset relative to the first quaternion's frame of reference – in our case, the frame of reference of the hand at the moment of grabbing.

+

We can then multiply those offsets by the current orientation of the hand:

+
if grabbedBox ~= nil then
+    local newPosition = handPosition - handOrientation * grabbedBox.positionOffset
+    local newOrientation = handOrientation * grabbedBox.orientationOffset
+
+    grabbedBox.box.position:set(newPosition)
+    grabbedBox.box.orientation:set(newOrientation)
+end
+
+

Notice how the position and orientation stay the same, if the hand didn't move:

+
newPosition = handPosition - handOrientation * grabbedBox.positionOffset
+newPosition = handPosition - handOrientation * quat(handOrientation_0):conjugate() * (handPosition_0 - box.position_0)
+newPosition = handPosition - identityQuat * (handPosition_0 - box.position_0)
+newPosition = handPosition - (handPosition_0 - box.position_0)
+newPosition = handPosition - handPosition_0 + box.position_0
+newPosition = zeroVector + box.position_0
+newPosition = box.position_0
+
+
newOrientation = handOrientation * grabbedBox.orientationOffset
+newOrientation = handOrientation * quat(handOrientation_0):conjugate() * box.orientation_0
+newOrientation = identityQuat * box.orientation_0
+newOrientation = box.orientation_0
+
+
+ +Whole code + +
local box1 = {
+    position = lovr.math.newVec3(0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.3, 0.3, 0.3),
+    orientation = lovr.math.newQuat(),
+}
+
+local box2 = {
+    position = lovr.math.newVec3(-0.5, 1, -0.5),
+    dimensions = lovr.math.newVec3(0.5, 0.5, 0.5),
+    orientation = lovr.math.newQuat(),
+}
+
+local boxes = { box1, box2 }
+
+local grabbedBoxes = {
+    ["hand/left"] = nil,
+    ["hand/right"] = nil,
+}
+
+function lovr.draw(pass)
+    for i, hand in ipairs(lovr.headset.getHands()) do
+        local handPosition = vec3(lovr.headset.getPosition(hand))
+        local handOrientation = quat(lovr.headset.getOrientation(hand))
+
+        local wasPressed = lovr.headset.wasPressed(hand, 'trigger')
+
+        if wasPressed then
+            for _, box in ipairs(boxes) do
+                if isPointInsideBox(handPosition, box) then
+                    local positionOffset = quat(handOrientation):conjugate() *
+                        (handPosition - box.position)
+                    local orientationOffset = quat(handOrientation):conjugate() * box.orientation
+
+                    grabbedBoxes[hand] = {
+                        box = box,
+                        positionOffset = lovr.math.newVec3(positionOffset),
+                        orientationOffset = lovr.math.newQuat(orientationOffset),
+                    }
+                end
+            end
+        end
+
+        local wasReleased = lovr.headset.wasReleased(hand, 'trigger')
+
+        if wasReleased then
+            grabbedBoxes[hand] = nil
+        end
+
+        local grabbedBox = grabbedBoxes[hand]
+
+        if grabbedBox ~= nil then
+            local newPosition = handPosition - handOrientation * grabbedBox.positionOffset
+            local newOrientation = handOrientation * grabbedBox.orientationOffset
+
+            grabbedBox.box.position:set(newPosition)
+            grabbedBox.box.orientation:set(newOrientation)
+        end
+
+        pass:sphere(handPosition, .1)
+    end
+
+    for _, box in ipairs(boxes) do
+        local isGrabbed = (
+            (grabbedBoxes["hand/left"] ~= nil and grabbedBoxes["hand/left"].box == box)
+            or (grabbedBoxes["hand/right"] ~= nil and grabbedBoxes["hand/right"].box == box)
+        )
+
+        if isGrabbed then
+            pass:setColor(0x00ff00)
+        else
+            pass:setColor(0xffffff)
+        end
+
+        pass:box(box.position, box.dimensions, box.orientation, 'line')
+    end
+end
+
+function isPointInsideBox(point, box)
+    local relativePoint = point - box.position + (box.dimensions / 2)
+    local width, height, depth = box.dimensions:unpack()
+
+    return (
+        relativePoint.x > 0 and relativePoint.x < width
+        and relativePoint.y > 0 and relativePoint.y < height
+        and relativePoint.z > 0 and relativePoint.z < depth
+    )
+end
+
+
+

Now you should have a working drag and drop with rotation!

+ + + +
+ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..f601375 --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + + + micouy's blog + + + +
+

+ micouy's blog +

+ +my github + + +
+ + diff --git a/mvp.css b/mvp.css new file mode 100644 index 0000000..da3d654 --- /dev/null +++ b/mvp.css @@ -0,0 +1,594 @@ +/* MVP.css v1.17 - https://github.com/andybrewer/mvp */ + +:root { + --active-brightness: 0.85; + --border-radius: 5px; + --box-shadow: 2px 2px 10px; + --color-accent: #118bee15; + --color-bg: #fff; + --color-bg-secondary: #e9e9e9; + --color-link: #118bee; + --color-secondary: #920de9; + --color-secondary-accent: #920de90b; + --color-shadow: #f4f4f4; + --color-table: #118bee; + --color-text: #000; + --color-text-secondary: #999; + --color-scrollbar: #cacae8; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + --hover-brightness: 1.2; + --justify-important: center; + --justify-normal: left; + --line-height: 1.5; + --width-card: 285px; + --width-card-medium: 460px; + --width-card-wide: 800px; + --width-content: 1080px; +} + +@media (prefers-color-scheme: dark) { + :root[color-mode="user"] { + --color-accent: #0097fc4f; + --color-bg: #333; + --color-bg-secondary: #555; + --color-link: #0097fc; + --color-secondary: #e20de9; + --color-secondary-accent: #e20de94f; + --color-shadow: #bbbbbb20; + --color-table: #0097fc; + --color-text: #f7f7f7; + --color-text-secondary: #aaa; + } +} + +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + +/* Layout */ +article aside { + background: var(--color-secondary-accent); + border-left: 4px solid var(--color-secondary); + padding: 0.01rem 0.8rem; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + line-height: var(--line-height); + margin: 0; + overflow-x: hidden; + padding: 0; +} + +footer, +header, +main { + margin: 0 auto; + max-width: var(--width-content); + padding: 3rem 1rem; +} + +hr { + background-color: var(--color-bg-secondary); + border: none; + height: 1px; + margin: 4rem 0; + width: 100%; +} + +section { + display: flex; + flex-wrap: wrap; + justify-content: var(--justify-important); +} + +section img, +article img { + max-width: 100%; +} + +section pre { + overflow: auto; +} + +section aside { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + margin: 1rem; + padding: 1.25rem; + width: var(--width-card); +} + +section aside:hover { + box-shadow: var(--box-shadow) var(--color-bg-secondary); +} + +[hidden] { + display: none; +} + +/* Headers */ +article header, +div header, +main header { + padding-top: 0; +} + +header { + text-align: var(--justify-important); +} + +header a b, +header a em, +header a i, +header a strong { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +header nav img { + margin: 1rem 0; +} + +section header { + padding-top: 0; + width: 100%; +} + +/* Nav */ +nav { + align-items: center; + display: flex; + font-weight: bold; + justify-content: space-between; + margin-bottom: 7rem; +} + +nav ul { + list-style: none; + padding: 0; +} + +nav ul li { + display: inline-block; + margin: 0 0.5rem; + position: relative; + text-align: left; +} + +/* Nav Dropdown */ +nav ul li:hover ul { + display: block; +} + +nav ul li ul { + background: var(--color-bg); + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: none; + height: auto; + left: -2px; + padding: 0.5rem 1rem; + position: absolute; + top: 1.7rem; + white-space: nowrap; + width: auto; + z-index: 1; +} + +nav ul li ul::before { + /* fill gap above to make mousing over them easier */ + content: ""; + position: absolute; + left: 0; + right: 0; + top: -0.5rem; + height: 0.5rem; +} + +nav ul li ul li, +nav ul li ul li a { + display: block; +} + +/* Nav for Mobile */ +@media (max-width: 768px) { + nav { + flex-wrap: wrap; + } + + nav ul li { + width: calc(100% - 1em); + } + + nav ul li ul { + border: none; + box-shadow: none; + display: block; + position: static; + } +} + +/* Typography */ +code, +samp { + background-color: var(--color-accent); + border-radius: var(--border-radius); + color: var(--color-text); + display: inline-block; + margin: 0 0.1rem; + padding: 0 0.5rem; +} + +details { + margin: 1.3rem 0; +} + +details summary { + font-weight: bold; + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: var(--line-height); + text-wrap: balance; +} + +mark { + padding: 0.1rem; +} + +ol li, +ul li { + padding: 0.2rem 0; +} + +p { + margin: 0.75rem 0; + padding: 0; + width: 100%; +} + +pre { + margin: 1rem 0; + max-width: var(--width-card-wide); + padding: 1rem 0; +} + +pre code, +pre samp { + display: block; + max-width: var(--width-card-wide); + padding: 0.5rem 2rem; + white-space: pre-wrap; +} + +small { + color: var(--color-text-secondary); +} + +sup { + background-color: var(--color-secondary); + border-radius: var(--border-radius); + color: var(--color-bg); + font-size: xx-small; + font-weight: bold; + margin: 0.2rem; + padding: 0.2rem 0.3rem; + position: relative; + top: -2px; +} + +/* Links */ +a { + color: var(--color-link); + display: inline-block; + font-weight: bold; + text-decoration: underline; +} + +a:hover { + filter: brightness(var(--hover-brightness)); +} + +a:active { + filter: brightness(var(--active-brightness)); +} + +a b, +a em, +a i, +a strong, +button, +input[type="submit"] { + border-radius: var(--border-radius); + display: inline-block; + font-size: medium; + font-weight: bold; + line-height: var(--line-height); + margin: 0.5rem 0; + padding: 1rem 2rem; +} + +button, +input[type="submit"] { + font-family: var(--font-family); +} + +button:hover, +input[type="submit"]:hover { + cursor: pointer; + filter: brightness(var(--hover-brightness)); +} + +button:active, +input[type="submit"]:active { + filter: brightness(var(--active-brightness)); +} + +a b, +a strong, +button, +input[type="submit"] { + background-color: var(--color-link); + border: 2px solid var(--color-link); + color: var(--color-bg); +} + +a em, +a i { + border: 2px solid var(--color-link); + border-radius: var(--border-radius); + color: var(--color-link); + display: inline-block; + padding: 1rem 2rem; +} + +article aside a { + color: var(--color-secondary); +} + +/* Images */ +figure { + margin: 0; + padding: 0; +} + +figure img { + max-width: 100%; +} + +figure figcaption { + color: var(--color-text-secondary); +} + +/* Forms */ +button:disabled, +input:disabled { + background: var(--color-bg-secondary); + border-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: not-allowed; +} + +button[disabled]:hover, +input[type="submit"][disabled]:hover { + filter: none; +} + +form { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: block; + max-width: var(--width-card-wide); + min-width: var(--width-card); + padding: 1.5rem; + text-align: var(--justify-normal); +} + +form header { + margin: 1.5rem 0; + padding: 1.5rem 0; +} + +input, +label, +select, +textarea { + display: block; + font-size: inherit; + max-width: var(--width-card-wide); +} + +input[type="checkbox"], +input[type="radio"] { + display: inline-block; +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + display: inline-block; + font-weight: normal; + position: relative; + top: 1px; +} + +input[type="range"] { + padding: 0.4rem 0; +} + +input, +select, +textarea { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + margin-bottom: 1rem; + padding: 0.4rem 0.8rem; +} + +input[type="text"], +input[type="password"], +textarea { + width: calc(100% - 1.6rem); +} + +input[readonly], +textarea[readonly] { + background-color: var(--color-bg-secondary); +} + +label { + font-weight: bold; + margin-bottom: 0.2rem; +} + +/* Popups */ +dialog { + max-width: 90%; + max-height: 85dvh; + margin: auto; + padding: 0; + border: 1px solid var(--color-bg-secondary); + border-radius: 0.5rem; + overscroll-behavior: contain; + scroll-behavior: smooth; + scrollbar-width: none; /* Hide scrollbar for Firefox */ + -ms-overflow-style: none; /* Hide scrollbar for IE and Edge */ + scrollbar-color: transparent transparent; + animation: bottom-to-top 0.25s ease-in-out forwards; +} + +dialog::-webkit-scrollbar { + width: 0; + display: none; +} + +dialog::-webkit-scrollbar-track { + background: transparent; +} + +dialog::-webkit-scrollbar-thumb { + background-color: transparent; +} + +@media (min-width: 650px) { + dialog { + max-width: 39rem; + } +} + +dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); +} + +@keyframes bottom-to-top { + 0% { + opacity: 0; + transform: translateY(10%); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +/* Tables */ +table { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + border-spacing: 0; + display: inline-block; + max-width: 100%; + overflow-x: auto; + padding: 0; + white-space: nowrap; +} + +table td, +table th, +table tr { + padding: 0.4rem 0.8rem; + text-align: var(--justify-important); +} + +table thead { + background-color: var(--color-table); + border-collapse: collapse; + border-radius: var(--border-radius); + color: var(--color-bg); + margin: 0; + padding: 0; +} + +table thead tr:first-child th:first-child { + border-top-left-radius: var(--border-radius); +} + +table thead tr:first-child th:last-child { + border-top-right-radius: var(--border-radius); +} + +table thead th:first-child, +table tr td:first-child { + text-align: var(--justify-normal); +} + +table tr:nth-child(even) { + background-color: var(--color-accent); +} + +/* Quotes */ +blockquote { + display: block; + font-size: x-large; + line-height: var(--line-height); + margin: 1rem auto; + max-width: var(--width-card-medium); + padding: 1.5rem 1rem; + text-align: var(--justify-important); +} + +blockquote footer { + color: var(--color-text-secondary); + display: block; + font-size: small; + line-height: var(--line-height); + padding: 1.5rem 0; +} + +/* Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-scrollbar) transparent; +} + +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: var(--color-scrollbar); + border-radius: 10px; +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..5b8030e --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: +Allow: / +Sitemap: https://micouy.github.io/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..1b5c8c4 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,10 @@ + + + + https://micouy.github.io/ + + + https://micouy.github.io/drag-and-drop-in-vr-with-lovr/ + 2024-11-29 + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..853ac29 --- /dev/null +++ b/style.css @@ -0,0 +1,3 @@ +body { + font-family: sans-serif; +}