Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions app/components/ImageScene.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<div ref="containerRef" class="image-scene" :class="{ 'is-loading': loading }">
<canvas ref="canvasRef" class="image-scene__canvas" />

<ClientOnly>
<div v-if="loading" class="image-scene__status">
Loading image…
</div>
<div v-else-if="error" class="image-scene__status image-scene__status--error">
{{ error }}
</div>
</ClientOnly>
</div>
</template>

<script setup>
import { toRef } from 'vue'

const props = defineProps({
src: {
type: String,
required: true,
},
// Background color of the 3D space (currently just informational — the
// renderer itself stays transparent so the page background shows through)
background: {
type: String,
default: '#0b0b0f',
},
tiltX: {
type: Number,
default: 0.35,
},
tiltY: {
type: Number,
default: -0.22,
},
baseDistance: {
type: Number,
default: 3.2,
},
zoomAmplitude: {
type: Number,
default: 10,
},
zoomSpeed: {
type: Number,
default: 0.4,
},
})

const { containerRef, canvasRef, loading, error, init } = useImageScene({
src: toRef(props, 'src'),
tiltX: props.tiltX,
tiltY: props.tiltY,
baseDistance: props.baseDistance,
zoomAmplitude: props.zoomAmplitude,
zoomSpeed: props.zoomSpeed,
})

// onMounted never runs during SSR, but guarding with import.meta.client
// makes the client-only intent explicit and protects against any future
// refactor (e.g. moving this call into a shared hook) that might not
// preserve that guarantee.
onMounted(() => {
if (import.meta.client) init()
})
</script>

<style scoped>
.image-scene {
position: relative;
width: 100%;
height: 100%;
min-height: 320px;
overflow: hidden;
border-radius: 8px;
}

.image-scene__canvas {
display: block;
width: 100%;
height: 100%;
}

.image-scene__status {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #d8d8de;
font: 500 14px/1.4 system-ui, sans-serif;
letter-spacing: 0.02em;
pointer-events: none;
}

.image-scene__status--error {
color: #ff8080;
}
</style>
86 changes: 86 additions & 0 deletions app/components/ThreeDFeatureBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<section
:class="{
'block-colored': feature.colored,
gradient: feature.gradient === true,
[`gradient-${gradientStep}`]: gradientStep,
block: !feature.colored,
'has-bg-image': feature.image
}"
>
<ClientOnly fallback-tag="span">

<ImageScene class="bg-image" :src="'/images/screenshots/' + feature.image" />

<template #fallback>
<NuxtImg
v-if="feature.image"
class="bg-image"
:src="'/images/screenshots/' + feature.image"
format="webp"
/>
</template>
</ClientOnly>

<section class="section content-layer">
<div
:class="{
tile: true,
'flex-aic': true,
content: !feature.reverted,
'content-reverse': feature.reverted
}"
>
<div class="tile is-5 is-child content" data-aos="fade-up">
<h2>
<span class="section-subtitle">
{{ feature.subtitle }}
</span>
<span class="section-title">
{{ feature.title }}
</span>
</h2>
<p>
{{ feature.content }}
</p>
</div>
</div>
</section>
</section>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
feature: Object
})

const feature = props.feature

const gradientStep = computed(() => {
if (typeof feature.gradient === 'number') return feature.gradient
return null
})
</script>

<style lang="stylus" scoped>
section
position relative
overflow hidden

.bg-image
position absolute
inset 0
width 100%
height 100%
object-fit cover
z-index 0

.content-layer
position relative
z-index 1

.screenshot
border-radius 10px
</style>
Loading