msdf splash text
This commit is contained in:
parent
2ba24ff7c2
commit
777b7f9a6d
22 changed files with 653 additions and 30 deletions
12
app/bin/generateMSDF.sh
Normal file
12
app/bin/generateMSDF.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
BASE_DIR=/home/nicwands/Documents/Dev/freelance/portfolio/braindrops.nicwands.com/app/
|
||||
|
||||
msdf-atlas-gen \
|
||||
-font "$BASE_DIR"/fonts/InstrumentSerif-Italic.ttf \
|
||||
-fontname 'instrument-serif' \
|
||||
-imageout instrument-serif-atlas.png \
|
||||
-json instrument-serif-atlas.json \
|
||||
-pxrange 14 \
|
||||
-dimensions 1024 1024 \
|
||||
-errorcorrection auto
|
||||
|
|
@ -14,12 +14,18 @@
|
|||
"singleQuote": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/tiny-sdf": "^2.2.0",
|
||||
"@tiptap/pm": "^3.28.0",
|
||||
"@tiptap/starter-kit": "^3.28.0",
|
||||
"@tiptap/vue-3": "^3.28.0",
|
||||
"@tresjs/cientos": "^5.8.1",
|
||||
"@tresjs/core": "^5.8.3",
|
||||
"fecha": "^4.2.3",
|
||||
"fontfaceobserver": "^2.3.0",
|
||||
"lodash": "^4.18.1",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"vite-plugin-glsl": "^1.6.0",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
|
|
|
|||
1
app/public/splash-title/hanken-grotesk-atlas.json
Normal file
1
app/public/splash-title/hanken-grotesk-atlas.json
Normal file
File diff suppressed because one or more lines are too long
BIN
app/public/splash-title/hanken-grotesk-atlas.png
Normal file
BIN
app/public/splash-title/hanken-grotesk-atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
1
app/public/splash-title/instrument-serif-atlas.json
Normal file
1
app/public/splash-title/instrument-serif-atlas.json
Normal file
File diff suppressed because one or more lines are too long
BIN
app/public/splash-title/instrument-serif-atlas.png
Normal file
BIN
app/public/splash-title/instrument-serif-atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 376 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<main class="container">
|
||||
<main :class="['container', { 'fonts-loaded': fontsLoaded }]">
|
||||
<router-view :manifest="manifest" :key="route.fullPath" />
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -7,18 +7,43 @@
|
|||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import loadFonts from '@/libs/loadFonts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const manifest = ref([])
|
||||
const fontsLoaded = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
manifest.value = await fetch('/manifest.json').then((r) => r.json())
|
||||
await loadFonts([
|
||||
{
|
||||
name: 'Instrument Serif',
|
||||
styles: ['italic'],
|
||||
weights: [400]
|
||||
},
|
||||
{
|
||||
name: 'Hanken Grotesk',
|
||||
styles: ['normal', 'italic'],
|
||||
weights: [400, 600]
|
||||
},
|
||||
{
|
||||
name: 'Iosevka Charon Mono',
|
||||
styles: ['normal'],
|
||||
weights: [700]
|
||||
},
|
||||
])
|
||||
fontsLoaded.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
background: var(--theme-layout);
|
||||
transition: opacity 600ms ease-out;
|
||||
|
||||
&:not(.fonts-loaded) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
280
app/src/components/SplashTitle.vue
Normal file
280
app/src/components/SplashTitle.vue
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
<template>
|
||||
<!-- Full-screen background plane for metaball visibility -->
|
||||
<TresMesh ref="bgPlane" :position="[0, 0, -0.1]">
|
||||
<TresPlaneGeometry :args="[10, 10]" />
|
||||
<TresShaderMaterial v-bind="bgMaterialProps" />
|
||||
</TresMesh>
|
||||
|
||||
<!-- Text mesh -->
|
||||
<TresMesh ref="textMesh">
|
||||
<TresBufferGeometry :ref="(el) => el && updateGeometry(el)" />
|
||||
<TresShaderMaterial v-bind="materialProps" />
|
||||
</TresMesh>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import vertShader from '@/libs/glsl/splash-title/vert.glsl'
|
||||
import fragShader from '@/libs/glsl/splash-title/frag.glsl'
|
||||
import useSmoothMouse from '@/composables/useSmoothMouse'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useLoop } from '@tresjs/core'
|
||||
import { shallowRef, onMounted, ref, watch } from 'vue'
|
||||
import { TextureLoader, LinearFilter, BufferAttribute } from 'three'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: 'Braindrops'
|
||||
},
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: 'instrument-serif'
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 0.2 // Scale factor relative to screen width
|
||||
},
|
||||
letterSpacing: {
|
||||
type: Number,
|
||||
default: 0 // Additional spacing in em units (can be negative)
|
||||
},
|
||||
blobStrength: {
|
||||
type: Number,
|
||||
default: 1.0 // Controls metaball merge distance (higher = merges from farther away)
|
||||
}
|
||||
})
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
const textMesh = shallowRef()
|
||||
const bgPlane = shallowRef()
|
||||
const atlasData = ref(null)
|
||||
const atlasTexture = shallowRef(null)
|
||||
|
||||
// Load MSDF atlas
|
||||
onMounted(async () => {
|
||||
const [jsonData, texture] = await Promise.all([
|
||||
fetch(`/splash-title/${props.fontFamily}-atlas.json`).then(r => r.json()),
|
||||
new TextureLoader().loadAsync(`/splash-title/${props.fontFamily}-atlas.png`)
|
||||
])
|
||||
|
||||
console.log(jsonData, texture)
|
||||
|
||||
atlasData.value = jsonData
|
||||
atlasTexture.value = texture
|
||||
texture.minFilter = LinearFilter
|
||||
texture.magFilter = LinearFilter
|
||||
texture.needsUpdate = true
|
||||
|
||||
// Trigger geometry update
|
||||
if (textMesh.value) {
|
||||
updateGeometry(textMesh.value.geometry)
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for props changes and update geometry
|
||||
watch(() => [props.text, props.fontFamily, props.fontSize, props.letterSpacing], () => {
|
||||
if (textMesh.value && atlasData.value) {
|
||||
updateGeometry(textMesh.value.geometry)
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for window size changes and update geometry
|
||||
watch([width, height], () => {
|
||||
if (textMesh.value && atlasData.value) {
|
||||
updateGeometry(textMesh.value.geometry)
|
||||
}
|
||||
})
|
||||
|
||||
// Create text layout and geometry
|
||||
function layoutText(text, fontData) {
|
||||
const glyphMap = new Map()
|
||||
fontData.glyphs.forEach(g => {
|
||||
glyphMap.set(g.unicode, g)
|
||||
})
|
||||
|
||||
const positions = []
|
||||
const uvs = []
|
||||
const indices = []
|
||||
|
||||
// First pass: measure total width including letter spacing
|
||||
let totalWidth = 0
|
||||
const chars = text.split('')
|
||||
chars.forEach((char, i) => {
|
||||
const code = char.charCodeAt(0)
|
||||
const glyph = glyphMap.get(code)
|
||||
if (glyph) {
|
||||
totalWidth += glyph.advance
|
||||
// Add letter spacing between characters (but not after the last one)
|
||||
if (i < chars.length - 1) {
|
||||
totalWidth += props.letterSpacing
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate scale to fit within desired width (responsive to screen aspect)
|
||||
const aspect = width.value / height.value
|
||||
const targetWidth = props.fontSize * 2 * aspect // Convert to normalized coordinates
|
||||
const scale = totalWidth > 0 ? targetWidth / totalWidth : 1
|
||||
|
||||
const atlasW = fontData.atlas.width
|
||||
const atlasH = fontData.atlas.height
|
||||
|
||||
let cursorX = 0
|
||||
let vertexCount = 0
|
||||
|
||||
chars.forEach((char, i) => {
|
||||
const code = char.charCodeAt(0)
|
||||
const glyph = glyphMap.get(code)
|
||||
|
||||
if (!glyph || !glyph.planeBounds) {
|
||||
cursorX += (glyph?.advance || 0) * scale
|
||||
// Add letter spacing
|
||||
if (i < chars.length - 1) {
|
||||
cursorX += props.letterSpacing * scale
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const pb = glyph.planeBounds
|
||||
const ab = glyph.atlasBounds
|
||||
|
||||
// Quad corners in plane space (scaled)
|
||||
const x0 = cursorX + pb.left * scale
|
||||
const y0 = pb.bottom * scale
|
||||
const x1 = cursorX + pb.right * scale
|
||||
const y1 = pb.top * scale
|
||||
|
||||
// UV coordinates (atlas space, Y flipped for WebGL)
|
||||
const u0 = ab.left / atlasW
|
||||
const v0 = 1.0 - ab.top / atlasH
|
||||
const u1 = ab.right / atlasW
|
||||
const v1 = 1.0 - ab.bottom / atlasH
|
||||
|
||||
// Vertex index offset for this quad
|
||||
const vIdx = vertexCount
|
||||
vertexCount += 4
|
||||
|
||||
// Four vertices (bottom-left, bottom-right, top-right, top-left)
|
||||
positions.push(
|
||||
x0, y0, 0,
|
||||
x1, y0, 0,
|
||||
x1, y1, 0,
|
||||
x0, y1, 0
|
||||
)
|
||||
|
||||
uvs.push(
|
||||
u0, v1,
|
||||
u1, v1,
|
||||
u1, v0,
|
||||
u0, v0
|
||||
)
|
||||
|
||||
// Two triangles
|
||||
indices.push(
|
||||
vIdx, vIdx + 1, vIdx + 2,
|
||||
vIdx, vIdx + 2, vIdx + 3
|
||||
)
|
||||
|
||||
// Advance cursor by glyph width plus letter spacing
|
||||
cursorX += glyph.advance * scale
|
||||
if (i < chars.length - 1) {
|
||||
cursorX += props.letterSpacing * scale
|
||||
}
|
||||
})
|
||||
|
||||
// Center the text
|
||||
const actualWidth = cursorX
|
||||
const offsetX = -actualWidth / 2
|
||||
for (let i = 0; i < positions.length; i += 3) {
|
||||
positions[i] += offsetX
|
||||
}
|
||||
|
||||
return { positions, uvs, indices, scale }
|
||||
}
|
||||
|
||||
// Update geometry when atlas is loaded
|
||||
function updateGeometry(geometry) {
|
||||
if (!atlasData.value) return
|
||||
|
||||
const { positions, uvs, indices, scale } = layoutText(props.text, atlasData.value)
|
||||
|
||||
geometry.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3))
|
||||
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2))
|
||||
geometry.setIndex(indices)
|
||||
geometry.computeBoundingBox()
|
||||
geometry.computeBoundingSphere()
|
||||
|
||||
// Update uniforms with current scale for MSDF quality
|
||||
if (textMesh.value?.material) {
|
||||
textMesh.value.material.uniforms.u_text_scale.value = scale
|
||||
}
|
||||
}
|
||||
|
||||
useSmoothMouse({
|
||||
normalize: true,
|
||||
onUpdate: ({ sx, sy }) => {
|
||||
const mousePos = [sx, 1 - sy]
|
||||
if (textMesh.value?.material) {
|
||||
textMesh.value.material.uniforms.u_mouse.value = mousePos
|
||||
}
|
||||
if (bgPlane.value?.material) {
|
||||
bgPlane.value.material.uniforms.u_mouse.value = mousePos
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const materialProps = {
|
||||
vertexShader: vertShader,
|
||||
fragmentShader: fragShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
uniforms: {
|
||||
u_resolution: { value: [width.value, height.value] },
|
||||
u_mouse: { value: [0.5, 0.5] },
|
||||
u_msdf: { value: atlasTexture.value },
|
||||
u_atlas_size: { value: [212, 212] },
|
||||
u_text_scale: { value: 1.0 },
|
||||
u_distance_range: { value: 2.0 }, // From atlas distanceRange
|
||||
u_is_text: { value: 1.0 }, // Flag to indicate this is text
|
||||
u_blob_strength: { value: props.blobStrength },
|
||||
},
|
||||
}
|
||||
|
||||
const bgMaterialProps = {
|
||||
vertexShader: vertShader,
|
||||
fragmentShader: fragShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
uniforms: {
|
||||
u_resolution: { value: [width.value, height.value] },
|
||||
u_mouse: { value: [0.5, 0.5] },
|
||||
u_msdf: { value: atlasTexture.value },
|
||||
u_atlas_size: { value: [212, 212] },
|
||||
u_text_scale: { value: 1.0 },
|
||||
u_distance_range: { value: 2.0 },
|
||||
u_is_text: { value: 0.0 }, // Background only shows metaball
|
||||
u_blob_strength: { value: props.blobStrength },
|
||||
},
|
||||
}
|
||||
|
||||
const { onRender } = useLoop()
|
||||
onRender(() => {
|
||||
const resolution = [width.value, height.value]
|
||||
|
||||
if (textMesh.value?.material) {
|
||||
textMesh.value.material.uniforms.u_resolution.value = resolution
|
||||
textMesh.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
||||
|
||||
// Update atlas texture reference if it changed
|
||||
if (atlasTexture.value && textMesh.value.material.uniforms.u_msdf.value !== atlasTexture.value) {
|
||||
textMesh.value.material.uniforms.u_msdf.value = atlasTexture.value
|
||||
}
|
||||
}
|
||||
|
||||
if (bgPlane.value?.material) {
|
||||
bgPlane.value.material.uniforms.u_resolution.value = resolution
|
||||
bgPlane.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
||||
}
|
||||
})
|
||||
</script>
|
||||
54
app/src/components/WebglCanvas.vue
Normal file
54
app/src/components/WebglCanvas.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="webgl-canvas">
|
||||
<TresCanvas
|
||||
v-bind="{
|
||||
powerPreference: 'high-performance',
|
||||
alpha: true,
|
||||
clearAlpha: 0,
|
||||
...options,
|
||||
}"
|
||||
>
|
||||
<!-- <TresOrthographicCamera
|
||||
:args="[0, 1, 0, 1, 0.001, 5000]"
|
||||
:position="[0, 0, -1000]"
|
||||
:look-at="[0, 0, 0]"
|
||||
/> -->
|
||||
<TresOrthographicCamera
|
||||
:args="[1 / -2, 1 / 2, 1 / 2, 1 / -2, 0.001, 5000]"
|
||||
:position="[0, 0, 1000]"
|
||||
/>
|
||||
|
||||
<TresAmbientLight />
|
||||
<TresPointLight />
|
||||
|
||||
<OrbitControls v-if="controls" />
|
||||
|
||||
<slot />
|
||||
</TresCanvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TresCanvas } from '@tresjs/core'
|
||||
import { OrbitControls } from '@tresjs/cientos'
|
||||
|
||||
const { controls, perspective } = defineProps({
|
||||
options: Object,
|
||||
controls: Boolean
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.webgl-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
&:not(.controls) {
|
||||
pointer-events: none;
|
||||
|
||||
.sticky canvas {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
app/src/composables/useSmoothMouse.js
Normal file
89
app/src/composables/useSmoothMouse.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import gsap from 'gsap'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useWindowSize, useEventListener } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* Shared global raw mouse state (only set up once)
|
||||
*/
|
||||
const rawMouse = {
|
||||
x: ref(0),
|
||||
y: ref(0),
|
||||
initialized: false,
|
||||
cleanup: null,
|
||||
}
|
||||
|
||||
const useGlobalMouseListener = () => {
|
||||
if (!rawMouse.initialized && !import.meta.env.SSR) {
|
||||
rawMouse.initialized = true
|
||||
rawMouse.cleanup = useEventListener(window, 'mousemove', (e) => {
|
||||
rawMouse.x.value = e.clientX
|
||||
rawMouse.y.value = e.clientY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for smoothed mouse position with customizable smoothing and normalization.
|
||||
*/
|
||||
export default (options) => {
|
||||
const smoothFactor = options?.smoothFactor ?? 0.1
|
||||
const normalize = options?.normalize ?? false
|
||||
const callback = options?.onUpdate
|
||||
|
||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
||||
|
||||
const sx = ref(0)
|
||||
const sy = ref(0)
|
||||
|
||||
useGlobalMouseListener()
|
||||
|
||||
const getTargetX = () =>
|
||||
normalize ? rawMouse.x.value / wWidth.value : rawMouse.x.value
|
||||
const getTargetY = () =>
|
||||
normalize ? rawMouse.y.value / wHeight.value : rawMouse.y.value
|
||||
|
||||
let tween
|
||||
onMounted(() => {
|
||||
if (tween) tween.kill
|
||||
|
||||
// Start smoothing tween
|
||||
tween = gsap.to(
|
||||
{ x: sx.value, y: sy.value },
|
||||
{
|
||||
duration: 1,
|
||||
ease: 'linear',
|
||||
repeat: -1,
|
||||
onUpdate: () => {
|
||||
const newX = gsap.utils.interpolate(
|
||||
sx.value,
|
||||
getTargetX(),
|
||||
smoothFactor,
|
||||
)
|
||||
const newY = gsap.utils.interpolate(
|
||||
sy.value,
|
||||
getTargetY(),
|
||||
smoothFactor,
|
||||
)
|
||||
|
||||
sx.value = newX
|
||||
sy.value = newY
|
||||
|
||||
callback?.({ sx: sx.value, sy: sy.value })
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
tween?.kill()
|
||||
rawMouse.cleanup()
|
||||
rawMouse.initialized = false
|
||||
})
|
||||
|
||||
return {
|
||||
sx,
|
||||
sy,
|
||||
rawX: rawMouse.x,
|
||||
rawY: rawMouse.y,
|
||||
}
|
||||
}
|
||||
57
app/src/libs/glsl/splash-title/frag.glsl
Normal file
57
app/src/libs/glsl/splash-title/frag.glsl
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
precision highp float;
|
||||
|
||||
varying vec2 v_uv;
|
||||
varying vec2 v_screen_uv;
|
||||
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec2 u_mouse;
|
||||
uniform sampler2D u_msdf;
|
||||
uniform vec2 u_atlas_size;
|
||||
uniform float u_text_scale;
|
||||
uniform float u_distance_range;
|
||||
uniform float u_is_text;
|
||||
uniform float u_blob_strength;
|
||||
|
||||
// MSDF median function - extracts signed distance from multi-channel texture
|
||||
float median(float r, float g, float b) {
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float aspect = u_resolution.x / u_resolution.y;
|
||||
|
||||
float tf = 0.0;
|
||||
|
||||
if (u_is_text > 0.5) {
|
||||
// Sample MSDF atlas - v_uv already contains the correct atlas coordinates
|
||||
vec3 msdfSample = texture2D(u_msdf, vec2(v_uv.x, 1. - v_uv.y)).rgb;
|
||||
float msdf = median(msdfSample.r, msdfSample.g, msdfSample.b);
|
||||
|
||||
// Proper MSDF distance field scaling for high quality rendering
|
||||
// Calculate screen-space derivatives to determine proper threshold
|
||||
vec2 unitRange = u_distance_range / u_atlas_size;
|
||||
vec2 screenTexSize = vec2(1.0) / fwidth(v_uv);
|
||||
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
||||
float screenPxDistance = screenPxRange * (msdf - 0.5);
|
||||
|
||||
// Convert MSDF distance to a field value centered at 0.5
|
||||
// This creates a smooth falloff around the glyph edge
|
||||
tf = screenPxDistance / screenPxRange + 0.5;
|
||||
}
|
||||
|
||||
// Mouse blob: smooth circular falloff in screen space, aspect-corrected
|
||||
float blobRadius = 0.05;
|
||||
vec2 d = (v_screen_uv - u_mouse) * vec2(aspect, 1.0);
|
||||
float bf = 1.0 - smoothstep(0.0, blobRadius, length(d));
|
||||
|
||||
// Metaball field sum: scale blob contribution to control merge distance
|
||||
// Higher u_blob_strength = blob merges from farther away
|
||||
// Lower u_blob_strength = blob must get closer to merge
|
||||
float field = tf + (bf * u_blob_strength);
|
||||
|
||||
// Anti-aliased edge at the 0.5 isosurface
|
||||
float fw = fwidth(field);
|
||||
float alpha = smoothstep(0.5 - fw, 0.5 + fw, field);
|
||||
|
||||
gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);
|
||||
}
|
||||
12
app/src/libs/glsl/splash-title/vert.glsl
Normal file
12
app/src/libs/glsl/splash-title/vert.glsl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
varying vec2 v_uv;
|
||||
varying vec2 v_screen_uv;
|
||||
|
||||
void main() {
|
||||
v_uv = uv;
|
||||
|
||||
// Compute screen-space UV for mouse interaction
|
||||
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
v_screen_uv = clipPos.xy / clipPos.w * 0.5 + 0.5;
|
||||
|
||||
gl_Position = clipPos;
|
||||
}
|
||||
50
app/src/libs/loadFonts.js
Normal file
50
app/src/libs/loadFonts.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import _uniq from 'lodash/uniq'
|
||||
import FontFaceObserver from 'fontfaceobserver'
|
||||
|
||||
const resolveWeights = (font) => {
|
||||
const declared = font.weights || []
|
||||
const single = font.weight ? [font.weight] : []
|
||||
const all = _uniq(declared.concat(single))
|
||||
return all.length ? all : [400]
|
||||
}
|
||||
const resolveStyles = (font) => {
|
||||
const declared = font.styles || []
|
||||
const single = font.style ? [font.style] : []
|
||||
const all = _uniq(declared.concat(single))
|
||||
return all.length ? all : ['normal']
|
||||
}
|
||||
|
||||
// Primary load function
|
||||
export default (ops) => {
|
||||
const fonts = Array.isArray(ops) ? ops : [ops]
|
||||
const families = []
|
||||
|
||||
// declare all fonts
|
||||
fonts.forEach((font) => {
|
||||
// resolve weights & styles
|
||||
const weights = resolveWeights(font)
|
||||
const styles = resolveStyles(font)
|
||||
|
||||
// split all into separate declarations
|
||||
weights.forEach((weight) => {
|
||||
styles.forEach((style) => {
|
||||
families.push({
|
||||
name: font.name,
|
||||
weight,
|
||||
style,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Load all in parallel
|
||||
return Promise.all(
|
||||
families.map((fam) => {
|
||||
const font = new FontFaceObserver(fam.name, {
|
||||
weight: fam.weight,
|
||||
style: fam.style,
|
||||
})
|
||||
return font.load()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,20 +1,40 @@
|
|||
<template>
|
||||
<main class="home theme-light">
|
||||
<h1>Braindrops</h1>
|
||||
<btn @click="onClick">View Drops</btn>
|
||||
<webgl-canvas>
|
||||
<splash-title
|
||||
v-if="ready"
|
||||
fontFamily="instrument-serif"
|
||||
:letterSpacing="-0.05"
|
||||
:blobStrength="1"
|
||||
/>
|
||||
</webgl-canvas>
|
||||
|
||||
<div class="content">
|
||||
<h1>Braindrops</h1>
|
||||
<btn @click="onClick">View Drops</btn>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Btn from '@shared/Btn.vue'
|
||||
import SplashTitle from '@/components/SplashTitle.vue'
|
||||
import WebglCanvas from '@/components/WebglCanvas.vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import Btn from '@shared/Btn.vue'
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
manifest: Array,
|
||||
})
|
||||
|
||||
const ready = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
document.fonts.ready.then(() => {
|
||||
ready.value = true
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
router.push(`/${props.manifest[0]}`)
|
||||
}
|
||||
|
|
@ -27,10 +47,21 @@ const onClick = () => {
|
|||
height: 100vh;
|
||||
padding-top: rs(20px);
|
||||
padding-bottom: rs(20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: rs(20px)
|
||||
|
||||
|
||||
.content {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: rs(20px);
|
||||
|
||||
h1 {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.webgl-canvas {
|
||||
z-index: 0;
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, mergeConfig } from 'vite'
|
||||
import sharedConfig from '../shared/vite.config.js'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import glsl from 'vite-plugin-glsl'
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(mergeConfig(sharedConfig, {
|
||||
...sharedConfig,
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
@ -13,7 +14,7 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
...sharedConfig.plugins,
|
||||
glsl(),
|
||||
{
|
||||
name: 'serve-drops',
|
||||
configureServer(server) {
|
||||
|
|
@ -62,4 +63,4 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}))
|
||||
|
|
|
|||
BIN
font-ttf/HankenGrotesk-Italic.ttf
Normal file
BIN
font-ttf/HankenGrotesk-Italic.ttf
Normal file
Binary file not shown.
BIN
font-ttf/HankenGrotesk-Regular.ttf
Normal file
BIN
font-ttf/HankenGrotesk-Regular.ttf
Normal file
Binary file not shown.
BIN
font-ttf/HankenGrotesk-SemiBold.ttf
Normal file
BIN
font-ttf/HankenGrotesk-SemiBold.ttf
Normal file
Binary file not shown.
BIN
font-ttf/InstrumentSerif-Italic.ttf
Normal file
BIN
font-ttf/InstrumentSerif-Italic.ttf
Normal file
Binary file not shown.
BIN
font-ttf/IosevkaCharonMono-Bold.ttf
Normal file
BIN
font-ttf/IosevkaCharonMono-Bold.ttf
Normal file
Binary file not shown.
35
package-lock.json
generated
35
package-lock.json
generated
|
|
@ -18,12 +18,18 @@
|
|||
"name": "braindrops-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@mapbox/tiny-sdf": "^2.2.0",
|
||||
"@tiptap/pm": "^3.28.0",
|
||||
"@tiptap/starter-kit": "^3.28.0",
|
||||
"@tiptap/vue-3": "^3.28.0",
|
||||
"@tresjs/cientos": "^5.8.1",
|
||||
"@tresjs/core": "^5.8.3",
|
||||
"fecha": "^4.2.3",
|
||||
"fontfaceobserver": "^2.3.0",
|
||||
"lodash": "^4.18.1",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"vite-plugin-glsl": "^1.6.0",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
|
|
@ -114,19 +120,6 @@
|
|||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"drops/testing": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fuzzco/font-loader": "^1.0.2",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
|
||||
|
|
@ -361,6 +354,12 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
|
||||
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||
|
|
@ -2324,6 +2323,12 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
|
@ -3232,10 +3237,6 @@
|
|||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/testing": {
|
||||
"resolved": "drops/testing",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.185.1",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
|
||||
|
|
|
|||
Loading…
Reference in a new issue