drop: bonsai
This commit is contained in:
parent
bbe9d7e889
commit
d243f4caea
81 changed files with 886 additions and 61 deletions
7
.zed/settings.json
Normal file
7
.zed/settings.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"formatter": "prettier",
|
||||||
|
"format_on_save": "on",
|
||||||
|
"prettier": {
|
||||||
|
"allowed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
BIN
app/public/drops/bonsai/assets/hanken-grotesk-BB3emo1n.woff2
Normal file
BIN
app/public/drops/bonsai/assets/hanken-grotesk-BB3emo1n.woff2
Normal file
Binary file not shown.
BIN
app/public/drops/bonsai/assets/hanken-grotesk-NweiADIQ.woff
Normal file
BIN
app/public/drops/bonsai/assets/hanken-grotesk-NweiADIQ.woff
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
app/public/drops/bonsai/assets/index-B4Fea1ir.css
Normal file
1
app/public/drops/bonsai/assets/index-B4Fea1ir.css
Normal file
File diff suppressed because one or more lines are too long
1
app/public/drops/bonsai/assets/index-kYUuDunm.js
Normal file
1
app/public/drops/bonsai/assets/index-kYUuDunm.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
BIN
app/public/drops/bonsai/bonsai.jpg
Normal file
BIN
app/public/drops/bonsai/bonsai.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
BIN
app/public/drops/bonsai/favicon.png
Normal file
BIN
app/public/drops/bonsai/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
14
app/public/drops/bonsai/index.html
Normal file
14
app/public/drops/bonsai/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/drops/bonsai/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bonsai</title>
|
||||||
|
<script type="module" crossorigin src="/drops/bonsai/assets/index-kYUuDunm.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/drops/bonsai/assets/index-B4Fea1ir.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
131
app/public/drops/bonsai/meta.json
Normal file
131
app/public/drops/bonsai/meta.json
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
{
|
||||||
|
"name": "bonsai",
|
||||||
|
"title": "Bonsai Rings",
|
||||||
|
"date": "2026-07-24",
|
||||||
|
"tags": [
|
||||||
|
"tag1",
|
||||||
|
"tag2",
|
||||||
|
"tag3"
|
||||||
|
],
|
||||||
|
"description": {
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Generates a canvas drawing representing the growth rings of a tree, configurable with parameters."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Rings: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "How many years of growth the tree has. More rings produce an older, denser cross-section."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Noise:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How bumpy or irregular each ring's outline is. Low values produce smooth, gently curved rings; high values create more jagged, uneven edges."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Variation:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How much each new ring's shape drifts away from the one before it. Near zero, all rings share the same basic silhouette; higher values let the tree's shape wander more freely as it ages."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Mutation:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How unevenly the tree grows around its circumference within a single year. Low values produce fairly uniform, concentric rings; high values create dramatic thick-and-thin sections within each ring."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "The seed for the generated rings is stored in the user's browser, so their tree will look the same across reloads."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"published": true
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/drops/endless-journal/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/drops/endless-journal/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Endless Journal</title>
|
<title>Endless Journal</title>
|
||||||
<script type="module" crossorigin src="/drops/endless-journal/assets/index-9EwT6NAf.js"></script>
|
<script type="module" crossorigin src="/drops/endless-journal/assets/index-CB8ifUrE.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/drops/endless-journal/assets/index-PMwlnwUr.css">
|
<link rel="stylesheet" crossorigin href="/drops/endless-journal/assets/index-iXXiVEt8.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/public/drops/flowmap/assets/index-DdbOMs3_.css
Normal file
1
app/public/drops/flowmap/assets/index-DdbOMs3_.css
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/drops/flowmap/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/drops/flowmap/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Flowmap</title>
|
<title>Flowmap</title>
|
||||||
<script type="module" crossorigin src="/drops/flowmap/assets/index-BgOfTPdu.js"></script>
|
<script type="module" crossorigin src="/drops/flowmap/assets/index-B9AhPugJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/drops/flowmap/assets/index-Ct_TWNmM.css">
|
<link rel="stylesheet" crossorigin href="/drops/flowmap/assets/index-DdbOMs3_.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/public/drops/image-melody/assets/index-Dk6TxSdE.css
Normal file
1
app/public/drops/image-melody/assets/index-Dk6TxSdE.css
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/drops/image-melody/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/drops/image-melody/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Image Melody</title>
|
<title>Image Melody</title>
|
||||||
<script type="module" crossorigin src="/drops/image-melody/assets/index-DOVFg_Mq.js"></script>
|
<script type="module" crossorigin src="/drops/image-melody/assets/index-C-dHDnAG.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/drops/image-melody/assets/index-CmoBfZze.css">
|
<link rel="stylesheet" crossorigin href="/drops/image-melody/assets/index-Dk6TxSdE.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/public/drops/noise-shader/assets/index-LBxPoCAA.css
Normal file
1
app/public/drops/noise-shader/assets/index-LBxPoCAA.css
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
|
@ -5,8 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/drops/noise-shader/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/drops/noise-shader/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Noise Shader</title>
|
<title>Noise Shader</title>
|
||||||
<script type="module" crossorigin src="/drops/noise-shader/assets/index-CVMeXlNb.js"></script>
|
<script type="module" crossorigin src="/drops/noise-shader/assets/index-CIpH8ME8.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/drops/noise-shader/assets/index-B21avM17.css">
|
<link rel="stylesheet" crossorigin href="/drops/noise-shader/assets/index-LBxPoCAA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
[
|
[
|
||||||
|
"bonsai",
|
||||||
"flowmap",
|
"flowmap",
|
||||||
"image-melody",
|
"image-melody",
|
||||||
"noise-shader",
|
"noise-shader",
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ onMounted(async () => {
|
||||||
manifest.value = await fetch('/manifest.json').then((r) => r.json())
|
manifest.value = await fetch('/manifest.json').then((r) => r.json())
|
||||||
await loadFonts([
|
await loadFonts([
|
||||||
{
|
{
|
||||||
name: 'Instrument Serif',
|
name: 'Galapagos A',
|
||||||
styles: ['italic'],
|
styles: ['normal'],
|
||||||
weights: [400]
|
weights: [400]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -33,15 +33,15 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0.2 // Scale factor relative to screen width
|
default: 0.2
|
||||||
},
|
},
|
||||||
letterSpacing: {
|
letterSpacing: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0 // Additional spacing in em units (can be negative)
|
default: 0
|
||||||
},
|
},
|
||||||
blobStrength: {
|
blobStrength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1.0 // Controls metaball merge distance (higher = merges from farther away)
|
default: 1.0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -238,6 +238,7 @@ const materialProps = {
|
||||||
u_distance_range: { value: 2.0 }, // From atlas distanceRange
|
u_distance_range: { value: 2.0 }, // From atlas distanceRange
|
||||||
u_is_text: { value: 1.0 }, // Flag to indicate this is text
|
u_is_text: { value: 1.0 }, // Flag to indicate this is text
|
||||||
u_blob_strength: { value: props.blobStrength },
|
u_blob_strength: { value: props.blobStrength },
|
||||||
|
u_time: { value: 0.0 },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,16 +256,18 @@ const bgMaterialProps = {
|
||||||
u_distance_range: { value: 2.0 },
|
u_distance_range: { value: 2.0 },
|
||||||
u_is_text: { value: 0.0 }, // Background only shows metaball
|
u_is_text: { value: 0.0 }, // Background only shows metaball
|
||||||
u_blob_strength: { value: props.blobStrength },
|
u_blob_strength: { value: props.blobStrength },
|
||||||
|
u_time: { value: 0.0 },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onRender } = useLoop()
|
const { onRender } = useLoop()
|
||||||
onRender(() => {
|
onRender(({ elapsed }) => {
|
||||||
const resolution = [width.value, height.value]
|
const resolution = [width.value, height.value]
|
||||||
|
|
||||||
if (textMesh.value?.material) {
|
if (textMesh.value?.material) {
|
||||||
textMesh.value.material.uniforms.u_resolution.value = resolution
|
textMesh.value.material.uniforms.u_resolution.value = resolution
|
||||||
textMesh.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
textMesh.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
||||||
|
textMesh.value.material.uniforms.u_time.value = elapsed
|
||||||
|
|
||||||
// Update atlas texture reference if it changed
|
// Update atlas texture reference if it changed
|
||||||
if (atlasTexture.value && textMesh.value.material.uniforms.u_msdf.value !== atlasTexture.value) {
|
if (atlasTexture.value && textMesh.value.material.uniforms.u_msdf.value !== atlasTexture.value) {
|
||||||
|
|
@ -275,6 +278,7 @@ onRender(() => {
|
||||||
if (bgPlane.value?.material) {
|
if (bgPlane.value?.material) {
|
||||||
bgPlane.value.material.uniforms.u_resolution.value = resolution
|
bgPlane.value.material.uniforms.u_resolution.value = resolution
|
||||||
bgPlane.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
bgPlane.value.material.uniforms.u_blob_strength.value = props.blobStrength
|
||||||
|
bgPlane.value.material.uniforms.u_time.value = elapsed
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,50 @@ uniform float u_text_scale;
|
||||||
uniform float u_distance_range;
|
uniform float u_distance_range;
|
||||||
uniform float u_is_text;
|
uniform float u_is_text;
|
||||||
uniform float u_blob_strength;
|
uniform float u_blob_strength;
|
||||||
|
uniform float u_time;
|
||||||
|
|
||||||
// MSDF median function - extracts signed distance from multi-channel texture
|
// MSDF median function - extracts signed distance from multi-channel texture
|
||||||
float median(float r, float g, float b) {
|
float median(float r, float g, float b) {
|
||||||
return max(min(r, g), min(max(r, g), b));
|
return max(min(r, g), min(max(r, g), b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash for noise
|
||||||
|
float hash(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth value noise
|
||||||
|
float noise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(
|
||||||
|
mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),
|
||||||
|
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),
|
||||||
|
u.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdUnevenCapsule( vec2 p, float r1, float r2, float h )
|
||||||
|
{
|
||||||
|
p.x = abs(p.x);
|
||||||
|
float b = (r1-r2)/h;
|
||||||
|
float a = sqrt(1.0-b*b);
|
||||||
|
float k = dot(p,vec2(-b,a));
|
||||||
|
if( k < 0.0 ) return length(p) - r1;
|
||||||
|
if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
|
||||||
|
return dot(p, vec2(a,b) ) - r1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layered noise for more organic blob edge
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float v = 0.0;
|
||||||
|
v += 0.500 * noise(p * 1.0);
|
||||||
|
v += 0.250 * noise(p * 2.0);
|
||||||
|
v += 0.125 * noise(p * 4.0);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
float aspect = u_resolution.x / u_resolution.y;
|
float aspect = u_resolution.x / u_resolution.y;
|
||||||
|
|
||||||
|
|
@ -28,30 +66,74 @@ void main() {
|
||||||
float msdf = median(msdfSample.r, msdfSample.g, msdfSample.b);
|
float msdf = median(msdfSample.r, msdfSample.g, msdfSample.b);
|
||||||
|
|
||||||
// Proper MSDF distance field scaling for high quality rendering
|
// 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 unitRange = u_distance_range / u_atlas_size;
|
||||||
vec2 screenTexSize = vec2(1.0) / fwidth(v_uv);
|
vec2 screenTexSize = vec2(1.0) / fwidth(v_uv);
|
||||||
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
||||||
float screenPxDistance = screenPxRange * (msdf - 0.5);
|
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;
|
tf = screenPxDistance / screenPxRange + 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse blob: smooth circular falloff in screen space, aspect-corrected
|
// Blob: uneven capsule SDF centered on the mouse, oriented vertically
|
||||||
float blobRadius = 0.05;
|
// r1 = bottom radius (larger), r2 = top radius (smaller), h = height
|
||||||
vec2 d = (v_screen_uv - u_mouse) * vec2(aspect, 1.0);
|
float r1 = 0.02;
|
||||||
float bf = 1.0 - smoothstep(0.0, blobRadius, length(d));
|
float r2 = 0.01;
|
||||||
|
float h = 0.03;
|
||||||
|
|
||||||
// Metaball field sum: scale blob contribution to control merge distance
|
// Aspect-corrected offset from mouse; shift p so capsule is centred on mouse
|
||||||
// Higher u_blob_strength = blob merges from farther away
|
vec2 d = (v_screen_uv - u_mouse) * vec2(aspect, 1.0);
|
||||||
// Lower u_blob_strength = blob must get closer to merge
|
vec2 capsuleP = d + vec2(0.0, h * 0.5); // sdUnevenCapsule base sits at origin
|
||||||
|
|
||||||
|
float sdf = sdUnevenCapsule(capsuleP, r1, r2, h);
|
||||||
|
|
||||||
|
// Add FBM noise directly to the SDF value to organically warp the boundary
|
||||||
|
vec2 noiseCoord = v_screen_uv * 5.0 + u_time * 0.25;
|
||||||
|
float edgeNoise = (fbm(noiseCoord) - 0.5) * 0.03; // centred, ±1.5% of screen
|
||||||
|
float noisySdf = sdf + edgeNoise;
|
||||||
|
|
||||||
|
// Convert SDF (neg=inside) to a 0→1 field matching the metaball convention
|
||||||
|
float softness = 0.008;
|
||||||
|
float bf = smoothstep(softness, -softness, noisySdf);
|
||||||
|
|
||||||
|
// Metaball field sum
|
||||||
float field = tf + (bf * u_blob_strength);
|
float field = tf + (bf * u_blob_strength);
|
||||||
|
|
||||||
// Anti-aliased edge at the 0.5 isosurface
|
// ── 3-D depth via screen-space normals ──────────────────────────────────
|
||||||
float fw = fwidth(field);
|
// The field gradient points toward the interior of each shape element.
|
||||||
|
// Treating the field as a height map gives a convex-bulge normal.
|
||||||
|
float fx = dFdx(field);
|
||||||
|
float fy = dFdy(field);
|
||||||
|
|
||||||
|
// z controls how "round" the surface appears; larger = flatter, smaller = steeper sides
|
||||||
|
vec3 normal = normalize(vec3(-fx, -fy, 0.1));
|
||||||
|
|
||||||
|
// Key light: upper-left, slightly toward viewer
|
||||||
|
vec3 lightDir = normalize(vec3(-0.4, 0.7, 1.0));
|
||||||
|
float diffuse = max(dot(normal, lightDir), 0.0);
|
||||||
|
|
||||||
|
// Rim / fill light from lower-right for depth separation
|
||||||
|
vec3 rimDir = normalize(vec3(0.6, -0.4, 0.5));
|
||||||
|
float rim = pow(max(dot(normal, rimDir), 0.0), 3.0) * 0.25;
|
||||||
|
|
||||||
|
// Specular (Blinn-Phong)
|
||||||
|
vec3 viewDir = vec3(0.0, 0.0, 1.0);
|
||||||
|
vec3 halfDir = normalize(lightDir + viewDir);
|
||||||
|
float specular = pow(max(dot(normal, halfDir), 0.0), 48.0);
|
||||||
|
|
||||||
|
// Base color derived from the surface normal's xy — shifts hue across the surface
|
||||||
|
// as the shape curves, giving an iridescent/sculpted feel
|
||||||
|
vec3 baseColor = vec3(
|
||||||
|
0.3 + normal.x * 0.6, // red channel: shifts with left/right curvature
|
||||||
|
0.2 + normal.y * 0.5, // green channel: shifts with up/down curvature
|
||||||
|
0.7 - normal.x * 0.3 // blue channel: inversely tracks red for contrast
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply lighting on top: darken with diffuse/rim, blow out to white with specular
|
||||||
|
vec3 color = baseColor * (0.1 + 0.7 * diffuse + rim) + vec3(0.7 * specular);
|
||||||
|
|
||||||
|
// Anti-aliased coverage mask at the 0.5 isosurface
|
||||||
|
float fw = fwidth(field);
|
||||||
float alpha = smoothstep(0.5 - fw, 0.5 + fw, field);
|
float alpha = smoothstep(0.5 - fw, 0.5 + fw, field);
|
||||||
|
|
||||||
gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);
|
gl_FragColor = vec4(color, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<splash-title
|
<splash-title
|
||||||
v-if="ready"
|
v-if="ready"
|
||||||
text="Braindrops"
|
text="Braindrops"
|
||||||
fontFamily="instrument-serif"
|
fontFamily="iosevka-charon"
|
||||||
:letterSpacing="-0.1"
|
:letterSpacing="-0.1"
|
||||||
:blobStrength="1"
|
:blobStrength="1"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
3
drops/bonsai/concept.md
Normal file
3
drops/bonsai/concept.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
- Growth rings
|
||||||
|
- seasonal change (lose leaves)
|
||||||
|
- ascii trees
|
||||||
13
drops/bonsai/index.html
Normal file
13
drops/bonsai/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bonsai</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
drops/bonsai/package.json
Normal file
30
drops/bonsai/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "bonsai",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^14.4.0",
|
||||||
|
"alea": "^1.0.1",
|
||||||
|
"intrinsic-scale": "^5.0.0",
|
||||||
|
"sass": "^1.98.0",
|
||||||
|
"sass-embedded": "^1.98.0",
|
||||||
|
"simplex-noise": "^4.0.3",
|
||||||
|
"uuid": "^14.0.1",
|
||||||
|
"vue": "^3.5.30"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
|
"vite": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
drops/bonsai/public/bonsai.jpg
Normal file
BIN
drops/bonsai/public/bonsai.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
BIN
drops/bonsai/public/favicon.png
Normal file
BIN
drops/bonsai/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
131
drops/bonsai/public/meta.json
Normal file
131
drops/bonsai/public/meta.json
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
{
|
||||||
|
"name": "bonsai",
|
||||||
|
"title": "Bonsai Rings",
|
||||||
|
"date": "2026-07-24",
|
||||||
|
"tags": [
|
||||||
|
"tag1",
|
||||||
|
"tag2",
|
||||||
|
"tag3"
|
||||||
|
],
|
||||||
|
"description": {
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Generates a canvas drawing representing the growth rings of a tree, configurable with parameters."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Rings: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "How many years of growth the tree has. More rings produce an older, denser cross-section."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Noise:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How bumpy or irregular each ring's outline is. Low values produce smooth, gently curved rings; high values create more jagged, uneven edges."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Variation:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How much each new ring's shape drifts away from the one before it. Near zero, all rings share the same basic silhouette; higher values let the tree's shape wander more freely as it ages."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Mutation:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " How unevenly the tree grows around its circumference within a single year. Low values produce fairly uniform, concentric rings; high values create dramatic thick-and-thin sections within each ring."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "The seed for the generated rings is stored in the user's browser, so their tree will look the same across reloads."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"published": true
|
||||||
|
}
|
||||||
17
drops/bonsai/src/App.vue
Normal file
17
drops/bonsai/src/App.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<main class="drop theme-light">
|
||||||
|
<Bonsai />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Bonsai from './components/Bonsai.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main.drop {
|
||||||
|
background: var(--theme-bg);
|
||||||
|
color: var(--theme-fg);
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
213
drops/bonsai/src/components/Bonsai.vue
Normal file
213
drops/bonsai/src/components/Bonsai.vue
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<div class="bonsai">
|
||||||
|
<canvas ref="canvas" />
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control">
|
||||||
|
<range
|
||||||
|
v-model="numRings"
|
||||||
|
label="Rings"
|
||||||
|
:min="2"
|
||||||
|
:max="80"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<range
|
||||||
|
v-model="noiseSpatialFreq"
|
||||||
|
label="Noise"
|
||||||
|
:min="0.1"
|
||||||
|
:max="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<range
|
||||||
|
v-model="noiseRingStep"
|
||||||
|
label="Variation"
|
||||||
|
:min="0"
|
||||||
|
:max="1.5"
|
||||||
|
:step="0.05"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<range
|
||||||
|
v-model="mutationStrength"
|
||||||
|
label="Mutation"
|
||||||
|
:step="0.05"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<btn @click="changeSeed">New Seed</btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watchEffect } from 'vue'
|
||||||
|
import { useElementSize, useStorage } from '@vueuse/core'
|
||||||
|
import { createNoise2D } from 'simplex-noise'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import Range from '@shared/Range.vue'
|
||||||
|
import Btn from '@shared/Btn.vue'
|
||||||
|
import alea from 'alea'
|
||||||
|
|
||||||
|
const canvas = ref()
|
||||||
|
const { width, height } = useElementSize(canvas)
|
||||||
|
|
||||||
|
// Seed — recreates the noise function whenever it changes
|
||||||
|
const seed = useStorage('bonsai-seed', '')
|
||||||
|
const changeSeed = () => {
|
||||||
|
seed.value = uuid()
|
||||||
|
}
|
||||||
|
if (!seed.value) changeSeed()
|
||||||
|
|
||||||
|
// Noise function is a computed so it rebuilds when the seed changes
|
||||||
|
const noise = computed(() => createNoise2D(alea(seed.value)))
|
||||||
|
|
||||||
|
// Persistent parameters
|
||||||
|
const numRings = ref(30)
|
||||||
|
const noiseSpatialFreq = ref(1)
|
||||||
|
const noiseRingStep = ref(0.35)
|
||||||
|
const mutationStrength = ref(0.4)
|
||||||
|
|
||||||
|
const TWO_PI = Math.PI * 2
|
||||||
|
const RESOLUTION = 360
|
||||||
|
const SECTION_ANGLE = TWO_PI / RESOLUTION
|
||||||
|
|
||||||
|
const generateRings = (maxRadius, noiseFunc, config) => {
|
||||||
|
const { numR, spatialFreq, ringStep, mutation } = config
|
||||||
|
const innerRadius = maxRadius * 0.04
|
||||||
|
|
||||||
|
// One growth amount per gap between consecutive rings
|
||||||
|
const numGaps = numR - 1
|
||||||
|
const rawGrowths = Array.from(
|
||||||
|
{ length: numGaps },
|
||||||
|
(_, g) => 0.15 + ((noiseFunc(g * 0.22, 1.5) + 1) / 2) * 1.7,
|
||||||
|
)
|
||||||
|
const totalRaw = rawGrowths.reduce((s, v) => s + v, 0)
|
||||||
|
const growthAmounts = rawGrowths.map(
|
||||||
|
(g) => (g / totalRaw) * (maxRadius - innerRadius),
|
||||||
|
)
|
||||||
|
|
||||||
|
const rings = []
|
||||||
|
|
||||||
|
// Innermost ring: small noisy circle
|
||||||
|
rings.push(
|
||||||
|
Array.from({ length: RESOLUTION + 1 }, (_, i) => {
|
||||||
|
const a = i * SECTION_ANGLE
|
||||||
|
const n = noiseFunc(
|
||||||
|
Math.cos(a) * spatialFreq,
|
||||||
|
Math.sin(a) * spatialFreq,
|
||||||
|
)
|
||||||
|
return innerRadius * (1 + n * 0.12)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Each subsequent ring derives its shape from the previous ring
|
||||||
|
for (let r = 1; r < numR; r++) {
|
||||||
|
const prev = rings[r - 1]
|
||||||
|
const base = growthAmounts[r - 1]
|
||||||
|
|
||||||
|
rings.push(
|
||||||
|
Array.from({ length: RESOLUTION + 1 }, (_, i) => {
|
||||||
|
const a = i * SECTION_ANGLE
|
||||||
|
const xN = Math.cos(a)
|
||||||
|
const yN = Math.sin(a)
|
||||||
|
const n = noiseFunc(
|
||||||
|
xN * spatialFreq + r * ringStep,
|
||||||
|
yN * spatialFreq + r * ringStep,
|
||||||
|
)
|
||||||
|
return prev[i] + base + n * base * mutation
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rings, growthAmounts }
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawRing = (ctx, radii) => {
|
||||||
|
ctx.beginPath()
|
||||||
|
for (let i = 0; i <= RESOLUTION; i++) {
|
||||||
|
const a = i * SECTION_ANGLE
|
||||||
|
const x = Math.cos(a) * radii[i]
|
||||||
|
const y = Math.sin(a) * radii[i]
|
||||||
|
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
if (!canvas.value || !width.value || !height.value) return
|
||||||
|
|
||||||
|
const ctx = canvas.value.getContext('2d')
|
||||||
|
const w = width.value
|
||||||
|
const h = height.value
|
||||||
|
|
||||||
|
// Reassigning width resets and clears the canvas (including any prior transforms)
|
||||||
|
canvas.value.width = w
|
||||||
|
canvas.value.height = h
|
||||||
|
|
||||||
|
const maxRadius = Math.min(w, h) * 0.46
|
||||||
|
ctx.translate(w / 2, h / 2)
|
||||||
|
|
||||||
|
const { rings, growthAmounts } = generateRings(maxRadius, noise.value, {
|
||||||
|
numR: numRings.value,
|
||||||
|
spatialFreq: noiseSpatialFreq.value,
|
||||||
|
ringStep: noiseRingStep.value,
|
||||||
|
mutation: mutationStrength.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const minGrowth = Math.min(...growthAmounts)
|
||||||
|
const maxGrowth = Math.max(...growthAmounts)
|
||||||
|
const growthRange = maxGrowth - minGrowth || 1
|
||||||
|
|
||||||
|
rings.forEach((ring, idx) => {
|
||||||
|
const g = growthAmounts[Math.max(0, idx - 1)]
|
||||||
|
const t = (g - minGrowth) / growthRange // 0 = slow/dry, 1 = fast/wet
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#000'
|
||||||
|
ctx.lineWidth = 0.8 + t * 1.4
|
||||||
|
|
||||||
|
drawRing(ctx, ring)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchEffect inside onMounted so canvas is available; automatically re-runs
|
||||||
|
// whenever any reactive value accessed inside draw() changes
|
||||||
|
onMounted(() => {
|
||||||
|
watchEffect(draw)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.bonsai {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--layout-margin);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: rs(16px);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: rs(6px);
|
||||||
|
min-width: rs(150px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
drops/bonsai/src/libs/math.js
Normal file
20
drops/bonsai/src/libs/math.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export function clamp(min, input, max) {
|
||||||
|
return Math.max(min, Math.min(input, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapRange(in_min, in_max, input, out_min, out_max) {
|
||||||
|
return ((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lerp(start, end, amt) {
|
||||||
|
return (1 - amt) * start + amt * end;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncate(value, decimals) {
|
||||||
|
return parseFloat(value.toFixed(decimals));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapValue(value, lowBound, highBound) {
|
||||||
|
const range = highBound - lowBound;
|
||||||
|
return ((((value - lowBound) % range) + range) % range) + lowBound;
|
||||||
|
}
|
||||||
6
drops/bonsai/src/main.js
Normal file
6
drops/bonsai/src/main.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import '@shared/styles/shared.scss'
|
||||||
|
import './styles/main.scss'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
1
drops/bonsai/src/styles/main.scss
Normal file
1
drops/bonsai/src/styles/main.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/* Drop specific styles */
|
||||||
21
drops/bonsai/vite.config.js
Normal file
21
drops/bonsai/vite.config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { defineConfig, mergeConfig } from 'vite'
|
||||||
|
import sharedConfig from '../../shared/vite.config.js'
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
mergeConfig(sharedConfig, {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@shared': fileURLToPath(
|
||||||
|
new URL('../../shared', import.meta.url),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: '/drops/bonsai/',
|
||||||
|
build: {
|
||||||
|
outDir: '../../app/public/drops/bonsai',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
83
package-lock.json
generated
83
package-lock.json
generated
|
|
@ -13,7 +13,10 @@
|
||||||
"drops/*",
|
"drops/*",
|
||||||
"cms",
|
"cms",
|
||||||
"backend"
|
"backend"
|
||||||
]
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.9.6"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"name": "braindrops-app",
|
"name": "braindrops-app",
|
||||||
|
|
@ -62,11 +65,16 @@
|
||||||
"vite": "^8.0.0"
|
"vite": "^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"drops/backend-bitch": {
|
"drops/bonsai": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^14.4.0",
|
||||||
|
"alea": "^1.0.1",
|
||||||
|
"intrinsic-scale": "^5.0.0",
|
||||||
"sass": "^1.98.0",
|
"sass": "^1.98.0",
|
||||||
"sass-embedded": "^1.98.0",
|
"sass-embedded": "^1.98.0",
|
||||||
|
"simplex-noise": "^4.0.3",
|
||||||
|
"uuid": "^14.0.1",
|
||||||
"vue": "^3.5.30"
|
"vue": "^3.5.30"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -1684,14 +1692,14 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/core": {
|
"node_modules/@vueuse/core": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
|
||||||
"integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
|
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
"@vueuse/metadata": "14.3.0",
|
"@vueuse/metadata": "14.4.0",
|
||||||
"@vueuse/shared": "14.3.0"
|
"@vueuse/shared": "14.4.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
|
@ -1701,18 +1709,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/metadata": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
|
||||||
"integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
|
"integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/shared": {
|
"node_modules/@vueuse/shared": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
|
||||||
"integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
|
"integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
|
@ -1733,6 +1741,12 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/alea": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ast-kit": {
|
"node_modules/ast-kit": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||||
|
|
@ -1779,10 +1793,6 @@
|
||||||
"node": ">=18.2.0"
|
"node": ">=18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/backend-bitch": {
|
|
||||||
"resolved": "drops/backend-bitch",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
|
|
@ -1792,6 +1802,10 @@
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bonsai": {
|
||||||
|
"resolved": "drops/bonsai",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/braindrops-app": {
|
"node_modules/braindrops-app": {
|
||||||
"resolved": "app",
|
"resolved": "app",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -2582,6 +2596,22 @@
|
||||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||||
|
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prosemirror-changeset": {
|
"node_modules/prosemirror-changeset": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||||||
|
|
@ -3212,6 +3242,12 @@
|
||||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/simplex-noise": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
@ -3473,6 +3509,19 @@
|
||||||
"integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
|
"integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/varint": {
|
"node_modules/varint": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,8 @@
|
||||||
"dev": "npm run build:drops && npm run dev -w app",
|
"dev": "npm run build:drops && npm run dev -w app",
|
||||||
"dev:backend": "npm run dev -w backend",
|
"dev:backend": "npm run dev -w backend",
|
||||||
"build": "npm run build:drops && npm run build -w app"
|
"build": "npm run build:drops && npm run build -w app"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.9.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
shared/Range.vue
Normal file
77
shared/Range.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="shared-range">
|
||||||
|
<label>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<span>{{ model }}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model.number="model"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
label: String,
|
||||||
|
min: { type: Number, default: 0 },
|
||||||
|
max: { type: Number, default: 1 },
|
||||||
|
step: { type: Number, default: 0.1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = defineModel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.shared-range {
|
||||||
|
label {
|
||||||
|
@include label;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: rs(8px);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
input[type='range'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: rs(2px);
|
||||||
|
background: var(--grey-400);
|
||||||
|
border-radius: rs(2px);
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: rs(12px);
|
||||||
|
height: rs(12px);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--theme-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 150ms var(--ease-out-quad);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
width: rs(12px);
|
||||||
|
height: rs(12px);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--theme-fg);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 150ms var(--ease-out-quad);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
shared/fonts/abc-galapagos-a-trial-black.woff
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-black.woff
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-black.woff2
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-black.woff2
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-bold.woff
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-bold.woff
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-bold.woff2
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-bold.woff2
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-light.woff
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-light.woff
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-light.woff2
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-light.woff2
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-semibold.woff
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-semibold.woff
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial-semibold.woff2
Normal file
BIN
shared/fonts/abc-galapagos-a-trial-semibold.woff2
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial.woff
Normal file
BIN
shared/fonts/abc-galapagos-a-trial.woff
Normal file
Binary file not shown.
BIN
shared/fonts/abc-galapagos-a-trial.woff2
Normal file
BIN
shared/fonts/abc-galapagos-a-trial.woff2
Normal file
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
||||||
@use "sass:color";
|
@use 'sass:color';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@each $name, $color in getColors() {
|
@each $name, $color in getColors() {
|
||||||
--#{$name}: #{$color};
|
--#{$name}: #{$color};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
900 - Black (Heavy)
|
900 - Black (Heavy)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Instrument Serif */
|
/* Galapagos A */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Instrument Serif';
|
font-family: 'Galapagos A';
|
||||||
font-style: italic;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src:
|
src:
|
||||||
url('@shared/fonts/instrument-serif-italic.woff2') format('woff2'),
|
url('@shared/fonts/abc-galapagos-a-trial.woff2') format('woff2'),
|
||||||
url('@shared/fonts/instrument-serif-italic.woff') format('woff');
|
url('@shared/fonts/abc-galapagos-a-trial.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hanken Grotesk */
|
/* Hanken Grotesk */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue