tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
fixed fluid text card
unbedenklich
2 weeks ago
cfe2893c
1e11c704
+192
-178
4 changed files
expand all
collapse all
unified
split
.claude
settings.local.json
src
lib
cards
FluidTextCard
FluidTextCard.svelte
FluidTextCardSettings.svelte
index.ts
+4
-1
.claude/settings.local.json
···
4
4
"Bash(pnpm check:*)",
5
5
"mcp__ide__getDiagnostics",
6
6
"mcp__plugin_svelte_svelte__svelte-autofixer",
7
7
-
"mcp__plugin_svelte_svelte__list-sections"
7
7
+
"mcp__plugin_svelte_svelte__list-sections",
8
8
+
"Bash(pkill:*)",
9
9
+
"Bash(timeout 8 pnpm dev:*)",
10
10
+
"Bash(git checkout:*)"
8
11
]
9
12
}
10
13
}
+168
-64
src/lib/cards/FluidTextCard/FluidTextCard.svelte
···
10
10
let maskCanvas: HTMLCanvasElement;
11
11
let animationId: number;
12
12
let splatIntervalId: ReturnType<typeof setInterval>;
13
13
+
let maskDrawRaf = 0;
14
14
+
let maskReady = false;
13
15
let isInitialized = $state(false);
14
16
let resizeObserver: ResizeObserver | null = null;
15
17
16
18
// Get text from card data
17
19
const text = $derived((item.cardData?.text as string) || 'hello');
18
18
-
const fontWeight = $derived((item.cardData?.fontWeight as string) || '900');
19
19
-
const fontFamily = $derived((item.cardData?.fontFamily as string) || 'Arial');
20
20
-
const fontSize = $derived((item.cardData?.fontSize as number) || 0.33);
20
20
+
const fontWeight = '900';
21
21
+
const fontFamily = 'Arial';
22
22
+
const fontSize = $derived((item.cardData?.fontSize as number) || 0.13);
21
23
22
24
// Draw text mask on overlay canvas
23
25
function drawOverlayCanvas() {
24
26
if (!maskCanvas || !container) return;
25
27
26
26
-
const rect = container.getBoundingClientRect();
27
27
-
if (rect.width === 0 || rect.height === 0) return;
28
28
+
const width = container.clientWidth;
29
29
+
const height = container.clientHeight;
30
30
+
if (width === 0 || height === 0) return;
28
31
29
32
const dpr = window.devicePixelRatio || 1;
30
33
31
31
-
maskCanvas.width = rect.width * dpr;
32
32
-
maskCanvas.height = rect.height * dpr;
34
34
+
maskCanvas.width = width * dpr;
35
35
+
maskCanvas.height = height * dpr;
33
36
34
37
const ctx = maskCanvas.getContext('2d')!;
38
38
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
39
39
+
ctx.globalCompositeOperation = 'source-over';
40
40
+
ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
35
41
ctx.scale(dpr, dpr);
36
42
37
43
ctx.fillStyle = 'black';
38
38
-
ctx.fillRect(0, 0, rect.width, rect.height);
44
44
+
ctx.fillRect(0, 0, width, height);
39
45
40
40
-
const textFontSize = Math.round(rect.width * fontSize);
41
41
-
ctx.font = fontWeight + ' ' + textFontSize + 'px ' + fontFamily;
46
46
+
// Font size as percentage of container width
47
47
+
const textFontSize = Math.round(width * fontSize);
48
48
+
ctx.font = `${fontWeight} ${textFontSize}px ${fontFamily}`;
42
49
43
50
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
44
51
ctx.lineWidth = 2;
45
45
-
ctx.textBaseline = 'middle';
46
52
ctx.textAlign = 'center';
47
53
48
48
-
ctx.strokeText(text, rect.width / 2, rect.height / 2);
54
54
+
const metrics = ctx.measureText(text);
55
55
+
let textY = height / 2;
56
56
+
if (
57
57
+
metrics.actualBoundingBoxAscent !== undefined &&
58
58
+
metrics.actualBoundingBoxDescent !== undefined
59
59
+
) {
60
60
+
ctx.textBaseline = 'alphabetic';
61
61
+
textY = (height + metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
62
62
+
} else {
63
63
+
ctx.textBaseline = 'middle';
64
64
+
}
65
65
+
66
66
+
ctx.strokeText(text, width / 2, textY);
49
67
ctx.globalCompositeOperation = 'destination-out';
50
50
-
ctx.fillText(text, rect.width / 2, rect.height / 2);
68
68
+
ctx.fillText(text, width / 2, textY);
69
69
+
ctx.globalCompositeOperation = 'source-over';
70
70
+
maskReady = true;
71
71
+
}
72
72
+
73
73
+
function scheduleMaskDraw() {
74
74
+
const width = container?.clientWidth ?? 0;
75
75
+
const height = container?.clientHeight ?? 0;
76
76
+
if (width > 0 && height > 0) {
77
77
+
drawOverlayCanvas();
78
78
+
return;
79
79
+
}
80
80
+
if (maskDrawRaf) return;
81
81
+
maskDrawRaf = requestAnimationFrame(() => {
82
82
+
maskDrawRaf = 0;
83
83
+
const nextWidth = container?.clientWidth ?? 0;
84
84
+
const nextHeight = container?.clientHeight ?? 0;
85
85
+
if (nextWidth === 0 || nextHeight === 0) {
86
86
+
scheduleMaskDraw();
87
87
+
return;
88
88
+
}
89
89
+
drawOverlayCanvas();
90
90
+
});
51
91
}
52
92
53
93
// Redraw overlay when text settings change (only after initialization)
54
94
$effect(() => {
55
95
// Access all reactive values to track them
56
96
text;
57
57
-
fontWeight;
58
58
-
fontFamily;
59
97
fontSize;
60
98
// Only redraw if already initialized
61
99
if (isInitialized) {
62
62
-
drawOverlayCanvas();
100
100
+
scheduleMaskDraw();
63
101
}
64
102
});
65
103
66
104
onMount(async () => {
67
105
// Wait for layout to settle
68
106
await tick();
69
69
-
initFluidSimulation();
107
107
+
// Wait for a frame to ensure dimensions are set
108
108
+
requestAnimationFrame(() => {
109
109
+
initFluidSimulation();
110
110
+
});
111
111
+
112
112
+
if (document.fonts?.ready) {
113
113
+
document.fonts.ready.then(() => {
114
114
+
if (isInitialized) scheduleMaskDraw();
115
115
+
});
116
116
+
}
70
117
});
71
118
72
119
onDestroy(() => {
73
120
if (animationId) cancelAnimationFrame(animationId);
74
121
if (splatIntervalId) clearInterval(splatIntervalId);
122
122
+
if (maskDrawRaf) cancelAnimationFrame(maskDrawRaf);
75
123
if (resizeObserver) resizeObserver.disconnect();
76
124
});
77
125
78
126
function initFluidSimulation() {
79
127
if (!fluidCanvas || !maskCanvas || !container) return;
80
128
81
81
-
drawOverlayCanvas();
129
129
+
maskReady = false;
130
130
+
scheduleMaskDraw();
82
131
83
132
// Simulation config
84
133
const config = {
···
123
172
deltaY: 0,
124
173
down: false,
125
174
moved: false,
126
126
-
color: [30, 0, 300] as [number, number, number]
175
175
+
color: [0, 0, 0] as [number, number, number]
127
176
};
128
177
}
129
178
···
163
212
if (!gl) return { gl: null, ext: { supportLinearFiltering: false } as any };
164
213
165
214
let halfFloat: any;
166
166
-
let supportLinearFiltering: any;
215
215
+
let supportLinearFiltering = false;
167
216
if (isWebGL2) {
168
217
gl.getExtension('EXT_color_buffer_float');
169
169
-
supportLinearFiltering = gl.getExtension('OES_texture_float_linear');
218
218
+
supportLinearFiltering = !!gl.getExtension('OES_texture_float_linear');
170
219
} else {
171
220
halfFloat = gl.getExtension('OES_texture_half_float');
172
172
-
supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear');
221
221
+
supportLinearFiltering = !!gl.getExtension('OES_texture_half_float_linear');
173
222
}
174
223
175
224
gl.clearColor(0.0, 0.0, 0.0, 1.0);
176
225
177
177
-
const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES;
226
226
+
let halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES;
227
227
+
let fallbackToUnsignedByte = false;
228
228
+
if (!halfFloatTexType) {
229
229
+
halfFloatTexType = gl.UNSIGNED_BYTE;
230
230
+
supportLinearFiltering = true;
231
231
+
fallbackToUnsignedByte = true;
232
232
+
}
178
233
let formatRGBA: any;
179
234
let formatRG: any;
180
235
let formatR: any;
181
236
182
237
if (isWebGL2) {
183
183
-
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);
184
184
-
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
185
185
-
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
238
238
+
if (fallbackToUnsignedByte) {
239
239
+
formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA };
240
240
+
formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA };
241
241
+
formatR = { internalFormat: gl.RGBA8, format: gl.RGBA };
242
242
+
} else {
243
243
+
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);
244
244
+
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
245
245
+
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
246
246
+
if (!formatRGBA) formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA };
247
247
+
if (!formatRG) formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA };
248
248
+
if (!formatR) formatR = { internalFormat: gl.RGBA8, format: gl.RGBA };
249
249
+
}
186
250
} else {
187
187
-
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
188
188
-
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
189
189
-
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
251
251
+
formatRGBA = { internalFormat: gl.RGBA, format: gl.RGBA };
252
252
+
formatRG = { internalFormat: gl.RGBA, format: gl.RGBA };
253
253
+
formatR = { internalFormat: gl.RGBA, format: gl.RGBA };
254
254
+
if (!fallbackToUnsignedByte) {
255
255
+
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRGBA;
256
256
+
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRG;
257
257
+
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatR;
258
258
+
}
190
259
}
191
260
192
261
return {
···
226
295
format: number,
227
296
type: number
228
297
) {
298
298
+
if (!type) return false;
229
299
const texture = gl.createTexture();
230
300
gl.bindTexture(gl.TEXTURE_2D, texture);
231
301
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
···
729
799
730
800
// Setup blit
731
801
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
732
732
-
gl.bufferData(
733
733
-
gl.ARRAY_BUFFER,
734
734
-
new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
735
735
-
gl.STATIC_DRAW
736
736
-
);
802
802
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW);
737
803
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
738
738
-
gl.bufferData(
739
739
-
gl.ELEMENT_ARRAY_BUFFER,
740
740
-
new Uint16Array([0, 1, 2, 0, 2, 3]),
741
741
-
gl.STATIC_DRAW
742
742
-
);
804
804
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
743
805
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
744
806
gl.enableVertexAttribArray(0);
745
807
···
1029
1091
texType,
1030
1092
gl.NEAREST
1031
1093
);
1032
1032
-
curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
1094
1094
+
curl = createFBO(
1095
1095
+
simRes.width,
1096
1096
+
simRes.height,
1097
1097
+
r.internalFormat,
1098
1098
+
r.format,
1099
1099
+
texType,
1100
1100
+
gl.NEAREST
1101
1101
+
);
1033
1102
pressure = createDoubleFBO(
1034
1103
simRes.width,
1035
1104
simRes.height,
···
1049
1118
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
1050
1119
1051
1120
sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering);
1052
1052
-
sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering);
1121
1121
+
sunraysTemp = createFBO(
1122
1122
+
res.width,
1123
1123
+
res.height,
1124
1124
+
r.internalFormat,
1125
1125
+
r.format,
1126
1126
+
texType,
1127
1127
+
filtering
1128
1128
+
);
1053
1129
}
1054
1130
1055
1131
function updateKeywords() {
···
1070
1146
if (fluidCanvas.width !== width || fluidCanvas.height !== height) {
1071
1147
fluidCanvas.width = width;
1072
1148
fluidCanvas.height = height;
1073
1073
-
drawOverlayCanvas();
1149
1149
+
scheduleMaskDraw();
1074
1150
return true;
1075
1151
}
1076
1152
return false;
···
1128
1204
return radius;
1129
1205
}
1130
1206
1131
1131
-
function splat(x: number, y: number, dx: number, dy: number, color: { r: number; g: number; b: number }) {
1207
1207
+
function splat(
1208
1208
+
x: number,
1209
1209
+
y: number,
1210
1210
+
dx: number,
1211
1211
+
dy: number,
1212
1212
+
color: { r: number; g: number; b: number }
1213
1213
+
) {
1132
1214
splatProgram.bind();
1133
1215
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
1134
1216
gl.uniform1f(splatProgram.uniforms.aspectRatio, fluidCanvas.width / fluidCanvas.height);
···
1328
1410
colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
1329
1411
pointers.forEach((p) => {
1330
1412
const c = generateColor();
1331
1331
-
p.color = [c.r * 255, c.g * 255, c.b * 255];
1413
1413
+
p.color = [c.r, c.g, c.b];
1332
1414
});
1333
1415
}
1334
1416
}
···
1355
1437
function update() {
1356
1438
const dt = calcDeltaTime() * (config.RENDER_SPEED ?? 1.0);
1357
1439
if (resizeCanvas()) initFramebuffers();
1440
1440
+
if (!maskReady) {
1441
1441
+
scheduleMaskDraw();
1442
1442
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
1443
1443
+
gl.clearColor(0.0, 0.0, 0.0, 1.0);
1444
1444
+
gl.clear(gl.COLOR_BUFFER_BIT);
1445
1445
+
animationId = requestAnimationFrame(update);
1446
1446
+
return;
1447
1447
+
}
1358
1448
updateColors(dt);
1359
1449
applyInputs();
1360
1450
if (!config.PAUSED) step(dt);
···
1395
1485
pointer.deltaX = 0;
1396
1486
pointer.deltaY = 0;
1397
1487
const c = generateColor();
1398
1398
-
pointer.color = [c.r * 255, c.g * 255, c.b * 255];
1488
1488
+
pointer.color = [c.r, c.g, c.b];
1399
1489
}
1400
1490
1401
1491
function updatePointerMoveData(pointer: Pointer, posX: number, posY: number) {
···
1412
1502
pointer.down = false;
1413
1503
}
1414
1504
1415
1415
-
// Event handlers
1416
1416
-
fluidCanvas.addEventListener('mouseenter', (e) => {
1505
1505
+
// Event handlers - use container so events work over both canvases
1506
1506
+
container.addEventListener('mouseenter', (e) => {
1417
1507
// Create a small burst when mouse enters the card
1418
1418
-
const posX = scaleByPixelRatio(e.offsetX);
1419
1419
-
const posY = scaleByPixelRatio(e.offsetY);
1508
1508
+
const rect = container.getBoundingClientRect();
1509
1509
+
const posX = scaleByPixelRatio(e.clientX - rect.left);
1510
1510
+
const posY = scaleByPixelRatio(e.clientY - rect.top);
1420
1511
const x = posX / fluidCanvas.width;
1421
1512
const y = 1.0 - posY / fluidCanvas.height;
1422
1513
const color = generateColor();
···
1426
1517
splat(x, y, 300 * (Math.random() - 0.5), 300 * (Math.random() - 0.5), color);
1427
1518
});
1428
1519
1429
1429
-
fluidCanvas.addEventListener('mousedown', (e) => {
1430
1430
-
const posX = scaleByPixelRatio(e.offsetX);
1431
1431
-
const posY = scaleByPixelRatio(e.offsetY);
1520
1520
+
container.addEventListener('mousedown', (e) => {
1521
1521
+
const rect = container.getBoundingClientRect();
1522
1522
+
const posX = scaleByPixelRatio(e.clientX - rect.left);
1523
1523
+
const posY = scaleByPixelRatio(e.clientY - rect.top);
1432
1524
let pointer = pointers.find((p) => p.id === -1);
1433
1525
if (!pointer) pointer = PointerPrototype();
1434
1526
updatePointerDownData(pointer, -1, posX, posY);
1435
1527
});
1436
1528
1437
1437
-
fluidCanvas.addEventListener('mousemove', (e) => {
1529
1529
+
container.addEventListener('mousemove', (e) => {
1438
1530
const pointer = pointers[0];
1439
1439
-
const posX = scaleByPixelRatio(e.offsetX);
1440
1440
-
const posY = scaleByPixelRatio(e.offsetY);
1531
1531
+
const rect = container.getBoundingClientRect();
1532
1532
+
const posX = scaleByPixelRatio(e.clientX - rect.left);
1533
1533
+
const posY = scaleByPixelRatio(e.clientY - rect.top);
1441
1534
updatePointerMoveData(pointer, posX, posY);
1442
1535
// Always create swish effect on hover
1443
1536
if (pointer.moved) {
1444
1537
pointer.moved = false;
1445
1538
// Generate a new color for visual interest
1446
1539
const c = generateColor();
1447
1447
-
pointer.color = [c.r * 255, c.g * 255, c.b * 255];
1448
1448
-
splatPointer(pointer);
1540
1540
+
pointer.color = [c.r, c.g, c.b];
1541
1541
+
splat(
1542
1542
+
pointer.texcoordX,
1543
1543
+
pointer.texcoordY,
1544
1544
+
pointer.deltaX * config.SPLAT_FORCE * 5,
1545
1545
+
pointer.deltaY * config.SPLAT_FORCE * 5,
1546
1546
+
{
1547
1547
+
r: pointer.color[0],
1548
1548
+
g: pointer.color[1],
1549
1549
+
b: pointer.color[2]
1550
1550
+
}
1551
1551
+
);
1449
1552
}
1450
1553
});
1451
1554
1452
1452
-
fluidCanvas.addEventListener('mouseup', () => {
1555
1555
+
container.addEventListener('mouseup', () => {
1453
1556
updatePointerUpData(pointers[0]);
1454
1557
});
1455
1558
1456
1456
-
fluidCanvas.addEventListener('touchstart', (e) => {
1559
1559
+
container.addEventListener('touchstart', (e) => {
1457
1560
e.preventDefault();
1458
1561
const touches = e.targetTouches;
1459
1562
while (touches.length >= pointers.length) pointers.push(PointerPrototype());
1460
1563
for (let i = 0; i < touches.length; i++) {
1461
1461
-
const rect = fluidCanvas.getBoundingClientRect();
1564
1564
+
const rect = container.getBoundingClientRect();
1462
1565
const posX = scaleByPixelRatio(touches[i].clientX - rect.left);
1463
1566
const posY = scaleByPixelRatio(touches[i].clientY - rect.top);
1464
1567
updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY);
1465
1568
}
1466
1569
});
1467
1570
1468
1468
-
fluidCanvas.addEventListener('touchmove', (e) => {
1571
1571
+
container.addEventListener('touchmove', (e) => {
1469
1572
e.preventDefault();
1470
1573
const touches = e.targetTouches;
1471
1574
for (let i = 0; i < touches.length; i++) {
1472
1575
const pointer = pointers[i + 1];
1473
1576
if (!pointer.down) continue;
1474
1474
-
const rect = fluidCanvas.getBoundingClientRect();
1577
1577
+
const rect = container.getBoundingClientRect();
1475
1578
const posX = scaleByPixelRatio(touches[i].clientX - rect.left);
1476
1579
const posY = scaleByPixelRatio(touches[i].clientY - rect.top);
1477
1580
updatePointerMoveData(pointer, posX, posY);
1478
1581
}
1479
1582
});
1480
1583
1481
1481
-
fluidCanvas.addEventListener('touchend', (e) => {
1584
1584
+
container.addEventListener('touchend', (e) => {
1482
1585
const touches = e.changedTouches;
1483
1586
for (let i = 0; i < touches.length; i++) {
1484
1587
const pointer = pointers.find((p) => p.id === touches[i].identifier);
···
1500
1603
// Resize observer - also triggers initial draw
1501
1604
resizeObserver = new ResizeObserver(() => {
1502
1605
resizeCanvas();
1503
1503
-
drawOverlayCanvas();
1606
1606
+
maskReady = false;
1607
1607
+
scheduleMaskDraw();
1504
1608
});
1505
1609
resizeObserver.observe(container);
1506
1610
+18
-109
src/lib/cards/FluidTextCard/FluidTextCardSettings.svelte
···
1
1
<script lang="ts">
2
2
import type { Item } from '$lib/types';
3
3
import type { ContentComponentProps } from '../types';
4
4
-
import { ToggleGroup, ToggleGroupItem, Button, Input, Label } from '@foxui/core';
4
4
+
import { Input, Label } from '@foxui/core';
5
5
6
6
let { item = $bindable<Item>() }: ContentComponentProps = $props();
7
7
8
8
-
const fontWeights = ['400', '500', '600', '700', '800', '900'] as const;
9
9
-
const fontFamilies = [
10
10
-
'Arial',
11
11
-
'Helvetica',
12
12
-
'Georgia',
13
13
-
'Times New Roman',
14
14
-
'Courier New',
15
15
-
'monospace'
16
16
-
] as const;
8
8
+
// Initialize fontSize if not set
9
9
+
if (item.cardData.fontSize === undefined) {
10
10
+
item.cardData.fontSize = 0.33;
11
11
+
}
17
12
18
18
-
const classes = 'size-8 min-w-8 text-xs cursor-pointer';
13
13
+
const displayPercent = $derived(Math.round((item.cardData.fontSize as number) * 100));
19
14
</script>
20
15
21
16
<div class="flex flex-col gap-3">
22
17
<div>
23
18
<Label class="mb-1 text-xs">Text</Label>
24
24
-
<Input
25
25
-
bind:value={item.cardData.text}
26
26
-
placeholder="Enter text"
27
27
-
class="w-full"
28
28
-
/>
29
29
-
</div>
30
30
-
31
31
-
<div>
32
32
-
<Label class="mb-1 text-xs">Font Weight</Label>
33
33
-
<ToggleGroup
34
34
-
type="single"
35
35
-
bind:value={
36
36
-
() => item.cardData.fontWeight ?? '900',
37
37
-
(value) => {
38
38
-
if (!value) return;
39
39
-
item.cardData.fontWeight = value;
40
40
-
}
41
41
-
}
42
42
-
>
43
43
-
{#each fontWeights as weight (weight)}
44
44
-
<ToggleGroupItem size="sm" value={weight} class={classes}>
45
45
-
{weight}
46
46
-
</ToggleGroupItem>
47
47
-
{/each}
48
48
-
</ToggleGroup>
19
19
+
<Input bind:value={item.cardData.text} placeholder="Enter text" class="w-full" />
49
20
</div>
50
21
51
22
<div>
52
52
-
<Label class="mb-1 text-xs">Font Family</Label>
53
53
-
<select
54
54
-
class="w-full rounded-md border border-base-200 bg-base-50 px-2 py-1.5 text-sm dark:border-base-800 dark:bg-base-900"
55
55
-
value={item.cardData.fontFamily ?? 'Arial'}
56
56
-
onchange={(e) => {
57
57
-
item.cardData.fontFamily = e.currentTarget.value;
23
23
+
<Label class="mb-1 text-xs">Font Size ({displayPercent}%)</Label>
24
24
+
<input
25
25
+
type="range"
26
26
+
min="0.1"
27
27
+
max="0.8"
28
28
+
step="0.01"
29
29
+
value={item.cardData.fontSize ?? 0.33}
30
30
+
oninput={(e) => {
31
31
+
item.cardData.fontSize = parseFloat(e.currentTarget.value);
58
32
}}
59
59
-
>
60
60
-
{#each fontFamilies as font (font)}
61
61
-
<option value={font}>{font}</option>
62
62
-
{/each}
63
63
-
</select>
64
64
-
</div>
65
65
-
66
66
-
<div>
67
67
-
<Label class="mb-1 text-xs">Font Size ({Math.round((item.cardData.fontSize ?? 0.33) * 100)}%)</Label>
68
68
-
<div class="flex items-center gap-2">
69
69
-
<Button
70
70
-
variant="ghost"
71
71
-
size="sm"
72
72
-
onclick={() => {
73
73
-
item.cardData.fontSize = Math.max((item.cardData.fontSize ?? 0.33) - 0.05, 0.1);
74
74
-
}}
75
75
-
disabled={(item.cardData.fontSize ?? 0.33) <= 0.1}
76
76
-
>
77
77
-
<svg
78
78
-
xmlns="http://www.w3.org/2000/svg"
79
79
-
width="16"
80
80
-
height="16"
81
81
-
viewBox="0 0 24 24"
82
82
-
fill="none"
83
83
-
stroke="currentColor"
84
84
-
stroke-width="2"
85
85
-
stroke-linecap="round"
86
86
-
stroke-linejoin="round"
87
87
-
>
88
88
-
<path d="M5 12h14" />
89
89
-
</svg>
90
90
-
</Button>
91
91
-
<input
92
92
-
type="range"
93
93
-
min="0.1"
94
94
-
max="0.8"
95
95
-
step="0.01"
96
96
-
value={item.cardData.fontSize ?? 0.33}
97
97
-
oninput={(e) => {
98
98
-
item.cardData.fontSize = parseFloat(e.currentTarget.value);
99
99
-
}}
100
100
-
class="h-2 w-full cursor-pointer appearance-none rounded-lg bg-base-200 dark:bg-base-700"
101
101
-
/>
102
102
-
<Button
103
103
-
variant="ghost"
104
104
-
size="sm"
105
105
-
onclick={() => {
106
106
-
item.cardData.fontSize = Math.min((item.cardData.fontSize ?? 0.33) + 0.05, 0.8);
107
107
-
}}
108
108
-
disabled={(item.cardData.fontSize ?? 0.33) >= 0.8}
109
109
-
>
110
110
-
<svg
111
111
-
xmlns="http://www.w3.org/2000/svg"
112
112
-
width="16"
113
113
-
height="16"
114
114
-
viewBox="0 0 24 24"
115
115
-
fill="none"
116
116
-
stroke="currentColor"
117
117
-
stroke-width="2"
118
118
-
stroke-linecap="round"
119
119
-
stroke-linejoin="round"
120
120
-
>
121
121
-
<path d="M5 12h14" />
122
122
-
<path d="M12 5v14" />
123
123
-
</svg>
124
124
-
</Button>
125
125
-
</div>
33
33
+
class="bg-base-200 dark:bg-base-700 h-2 w-full cursor-pointer appearance-none rounded-lg"
34
34
+
/>
126
35
</div>
127
36
</div>
+2
-4
src/lib/cards/FluidTextCard/index.ts
···
9
9
createNew: (card) => {
10
10
card.cardType = 'fluid-text';
11
11
card.cardData = {
12
12
-
text: '',
13
13
-
fontWeight: '900',
14
14
-
fontFamily: 'Arial',
15
15
-
fontSize: 0.33
12
12
+
text: ''
16
13
};
17
14
card.w = 4;
18
15
card.h = 2;
···
23
20
settingsComponent: FluidTextCardSettings,
24
21
sidebarButtonText: 'Fluid Text',
25
22
defaultColor: 'transparent',
23
23
+
allowSetColor: false,
26
24
minW: 2,
27
25
minH: 2
28
26
} as CardDefinition & { type: 'fluid-text' };