your personal website on atproto - mirror blento.app

fixed fluid text card

+192 -178
+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 - "mcp__plugin_svelte_svelte__list-sections" 7 + "mcp__plugin_svelte_svelte__list-sections", 8 + "Bash(pkill:*)", 9 + "Bash(timeout 8 pnpm dev:*)", 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 + let maskDrawRaf = 0; 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 - const fontWeight = $derived((item.cardData?.fontWeight as string) || '900'); 19 - const fontFamily = $derived((item.cardData?.fontFamily as string) || 'Arial'); 20 - const fontSize = $derived((item.cardData?.fontSize as number) || 0.33); 20 + const fontWeight = '900'; 21 + const fontFamily = 'Arial'; 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 - const rect = container.getBoundingClientRect(); 27 - if (rect.width === 0 || rect.height === 0) return; 28 + const width = container.clientWidth; 29 + const height = container.clientHeight; 30 + if (width === 0 || height === 0) return; 28 31 29 32 const dpr = window.devicePixelRatio || 1; 30 33 31 - maskCanvas.width = rect.width * dpr; 32 - maskCanvas.height = rect.height * dpr; 34 + maskCanvas.width = width * dpr; 35 + maskCanvas.height = height * dpr; 33 36 34 37 const ctx = maskCanvas.getContext('2d')!; 38 + ctx.setTransform(1, 0, 0, 1, 0, 0); 39 + ctx.globalCompositeOperation = 'source-over'; 40 + ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); 35 41 ctx.scale(dpr, dpr); 36 42 37 43 ctx.fillStyle = 'black'; 38 - ctx.fillRect(0, 0, rect.width, rect.height); 44 + ctx.fillRect(0, 0, width, height); 39 45 40 - const textFontSize = Math.round(rect.width * fontSize); 41 - ctx.font = fontWeight + ' ' + textFontSize + 'px ' + fontFamily; 46 + // Font size as percentage of container width 47 + const textFontSize = Math.round(width * fontSize); 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 - ctx.textBaseline = 'middle'; 46 52 ctx.textAlign = 'center'; 47 53 48 - ctx.strokeText(text, rect.width / 2, rect.height / 2); 54 + const metrics = ctx.measureText(text); 55 + let textY = height / 2; 56 + if ( 57 + metrics.actualBoundingBoxAscent !== undefined && 58 + metrics.actualBoundingBoxDescent !== undefined 59 + ) { 60 + ctx.textBaseline = 'alphabetic'; 61 + textY = (height + metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2; 62 + } else { 63 + ctx.textBaseline = 'middle'; 64 + } 65 + 66 + ctx.strokeText(text, width / 2, textY); 49 67 ctx.globalCompositeOperation = 'destination-out'; 50 - ctx.fillText(text, rect.width / 2, rect.height / 2); 68 + ctx.fillText(text, width / 2, textY); 69 + ctx.globalCompositeOperation = 'source-over'; 70 + maskReady = true; 71 + } 72 + 73 + function scheduleMaskDraw() { 74 + const width = container?.clientWidth ?? 0; 75 + const height = container?.clientHeight ?? 0; 76 + if (width > 0 && height > 0) { 77 + drawOverlayCanvas(); 78 + return; 79 + } 80 + if (maskDrawRaf) return; 81 + maskDrawRaf = requestAnimationFrame(() => { 82 + maskDrawRaf = 0; 83 + const nextWidth = container?.clientWidth ?? 0; 84 + const nextHeight = container?.clientHeight ?? 0; 85 + if (nextWidth === 0 || nextHeight === 0) { 86 + scheduleMaskDraw(); 87 + return; 88 + } 89 + drawOverlayCanvas(); 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 - fontWeight; 58 - fontFamily; 59 97 fontSize; 60 98 // Only redraw if already initialized 61 99 if (isInitialized) { 62 - drawOverlayCanvas(); 100 + scheduleMaskDraw(); 63 101 } 64 102 }); 65 103 66 104 onMount(async () => { 67 105 // Wait for layout to settle 68 106 await tick(); 69 - initFluidSimulation(); 107 + // Wait for a frame to ensure dimensions are set 108 + requestAnimationFrame(() => { 109 + initFluidSimulation(); 110 + }); 111 + 112 + if (document.fonts?.ready) { 113 + document.fonts.ready.then(() => { 114 + if (isInitialized) scheduleMaskDraw(); 115 + }); 116 + } 70 117 }); 71 118 72 119 onDestroy(() => { 73 120 if (animationId) cancelAnimationFrame(animationId); 74 121 if (splatIntervalId) clearInterval(splatIntervalId); 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 - drawOverlayCanvas(); 129 + maskReady = false; 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 - color: [30, 0, 300] as [number, number, number] 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 - let supportLinearFiltering: any; 215 + let supportLinearFiltering = false; 167 216 if (isWebGL2) { 168 217 gl.getExtension('EXT_color_buffer_float'); 169 - supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); 218 + supportLinearFiltering = !!gl.getExtension('OES_texture_float_linear'); 170 219 } else { 171 220 halfFloat = gl.getExtension('OES_texture_half_float'); 172 - supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); 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 - const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES; 226 + let halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES; 227 + let fallbackToUnsignedByte = false; 228 + if (!halfFloatTexType) { 229 + halfFloatTexType = gl.UNSIGNED_BYTE; 230 + supportLinearFiltering = true; 231 + fallbackToUnsignedByte = true; 232 + } 178 233 let formatRGBA: any; 179 234 let formatRG: any; 180 235 let formatR: any; 181 236 182 237 if (isWebGL2) { 183 - formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); 184 - formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); 185 - formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); 238 + if (fallbackToUnsignedByte) { 239 + formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA }; 240 + formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA }; 241 + formatR = { internalFormat: gl.RGBA8, format: gl.RGBA }; 242 + } else { 243 + formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); 244 + formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); 245 + formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); 246 + if (!formatRGBA) formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA }; 247 + if (!formatRG) formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA }; 248 + if (!formatR) formatR = { internalFormat: gl.RGBA8, format: gl.RGBA }; 249 + } 186 250 } else { 187 - formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 188 - formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 189 - formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 251 + formatRGBA = { internalFormat: gl.RGBA, format: gl.RGBA }; 252 + formatRG = { internalFormat: gl.RGBA, format: gl.RGBA }; 253 + formatR = { internalFormat: gl.RGBA, format: gl.RGBA }; 254 + if (!fallbackToUnsignedByte) { 255 + formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRGBA; 256 + formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRG; 257 + formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatR; 258 + } 190 259 } 191 260 192 261 return { ··· 226 295 format: number, 227 296 type: number 228 297 ) { 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 - gl.bufferData( 733 - gl.ARRAY_BUFFER, 734 - new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), 735 - gl.STATIC_DRAW 736 - ); 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 - gl.bufferData( 739 - gl.ELEMENT_ARRAY_BUFFER, 740 - new Uint16Array([0, 1, 2, 0, 2, 3]), 741 - gl.STATIC_DRAW 742 - ); 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 - curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); 1094 + curl = createFBO( 1095 + simRes.width, 1096 + simRes.height, 1097 + r.internalFormat, 1098 + r.format, 1099 + texType, 1100 + gl.NEAREST 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 - sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); 1121 + sunraysTemp = createFBO( 1122 + res.width, 1123 + res.height, 1124 + r.internalFormat, 1125 + r.format, 1126 + texType, 1127 + filtering 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 - drawOverlayCanvas(); 1149 + scheduleMaskDraw(); 1074 1150 return true; 1075 1151 } 1076 1152 return false; ··· 1128 1204 return radius; 1129 1205 } 1130 1206 1131 - function splat(x: number, y: number, dx: number, dy: number, color: { r: number; g: number; b: number }) { 1207 + function splat( 1208 + x: number, 1209 + y: number, 1210 + dx: number, 1211 + dy: number, 1212 + color: { r: number; g: number; b: number } 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 - p.color = [c.r * 255, c.g * 255, c.b * 255]; 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 + if (!maskReady) { 1441 + scheduleMaskDraw(); 1442 + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 1443 + gl.clearColor(0.0, 0.0, 0.0, 1.0); 1444 + gl.clear(gl.COLOR_BUFFER_BIT); 1445 + animationId = requestAnimationFrame(update); 1446 + return; 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 - pointer.color = [c.r * 255, c.g * 255, c.b * 255]; 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 - // Event handlers 1416 - fluidCanvas.addEventListener('mouseenter', (e) => { 1505 + // Event handlers - use container so events work over both canvases 1506 + container.addEventListener('mouseenter', (e) => { 1417 1507 // Create a small burst when mouse enters the card 1418 - const posX = scaleByPixelRatio(e.offsetX); 1419 - const posY = scaleByPixelRatio(e.offsetY); 1508 + const rect = container.getBoundingClientRect(); 1509 + const posX = scaleByPixelRatio(e.clientX - rect.left); 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 - fluidCanvas.addEventListener('mousedown', (e) => { 1430 - const posX = scaleByPixelRatio(e.offsetX); 1431 - const posY = scaleByPixelRatio(e.offsetY); 1520 + container.addEventListener('mousedown', (e) => { 1521 + const rect = container.getBoundingClientRect(); 1522 + const posX = scaleByPixelRatio(e.clientX - rect.left); 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 - fluidCanvas.addEventListener('mousemove', (e) => { 1529 + container.addEventListener('mousemove', (e) => { 1438 1530 const pointer = pointers[0]; 1439 - const posX = scaleByPixelRatio(e.offsetX); 1440 - const posY = scaleByPixelRatio(e.offsetY); 1531 + const rect = container.getBoundingClientRect(); 1532 + const posX = scaleByPixelRatio(e.clientX - rect.left); 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 - pointer.color = [c.r * 255, c.g * 255, c.b * 255]; 1448 - splatPointer(pointer); 1540 + pointer.color = [c.r, c.g, c.b]; 1541 + splat( 1542 + pointer.texcoordX, 1543 + pointer.texcoordY, 1544 + pointer.deltaX * config.SPLAT_FORCE * 5, 1545 + pointer.deltaY * config.SPLAT_FORCE * 5, 1546 + { 1547 + r: pointer.color[0], 1548 + g: pointer.color[1], 1549 + b: pointer.color[2] 1550 + } 1551 + ); 1449 1552 } 1450 1553 }); 1451 1554 1452 - fluidCanvas.addEventListener('mouseup', () => { 1555 + container.addEventListener('mouseup', () => { 1453 1556 updatePointerUpData(pointers[0]); 1454 1557 }); 1455 1558 1456 - fluidCanvas.addEventListener('touchstart', (e) => { 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 - const rect = fluidCanvas.getBoundingClientRect(); 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 - fluidCanvas.addEventListener('touchmove', (e) => { 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 - const rect = fluidCanvas.getBoundingClientRect(); 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 - fluidCanvas.addEventListener('touchend', (e) => { 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 - drawOverlayCanvas(); 1606 + maskReady = false; 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 - import { ToggleGroup, ToggleGroupItem, Button, Input, Label } from '@foxui/core'; 4 + import { Input, Label } from '@foxui/core'; 5 5 6 6 let { item = $bindable<Item>() }: ContentComponentProps = $props(); 7 7 8 - const fontWeights = ['400', '500', '600', '700', '800', '900'] as const; 9 - const fontFamilies = [ 10 - 'Arial', 11 - 'Helvetica', 12 - 'Georgia', 13 - 'Times New Roman', 14 - 'Courier New', 15 - 'monospace' 16 - ] as const; 8 + // Initialize fontSize if not set 9 + if (item.cardData.fontSize === undefined) { 10 + item.cardData.fontSize = 0.33; 11 + } 17 12 18 - const classes = 'size-8 min-w-8 text-xs cursor-pointer'; 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 - <Input 25 - bind:value={item.cardData.text} 26 - placeholder="Enter text" 27 - class="w-full" 28 - /> 29 - </div> 30 - 31 - <div> 32 - <Label class="mb-1 text-xs">Font Weight</Label> 33 - <ToggleGroup 34 - type="single" 35 - bind:value={ 36 - () => item.cardData.fontWeight ?? '900', 37 - (value) => { 38 - if (!value) return; 39 - item.cardData.fontWeight = value; 40 - } 41 - } 42 - > 43 - {#each fontWeights as weight (weight)} 44 - <ToggleGroupItem size="sm" value={weight} class={classes}> 45 - {weight} 46 - </ToggleGroupItem> 47 - {/each} 48 - </ToggleGroup> 19 + <Input bind:value={item.cardData.text} placeholder="Enter text" class="w-full" /> 49 20 </div> 50 21 51 22 <div> 52 - <Label class="mb-1 text-xs">Font Family</Label> 53 - <select 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 - value={item.cardData.fontFamily ?? 'Arial'} 56 - onchange={(e) => { 57 - item.cardData.fontFamily = e.currentTarget.value; 23 + <Label class="mb-1 text-xs">Font Size ({displayPercent}%)</Label> 24 + <input 25 + type="range" 26 + min="0.1" 27 + max="0.8" 28 + step="0.01" 29 + value={item.cardData.fontSize ?? 0.33} 30 + oninput={(e) => { 31 + item.cardData.fontSize = parseFloat(e.currentTarget.value); 58 32 }} 59 - > 60 - {#each fontFamilies as font (font)} 61 - <option value={font}>{font}</option> 62 - {/each} 63 - </select> 64 - </div> 65 - 66 - <div> 67 - <Label class="mb-1 text-xs">Font Size ({Math.round((item.cardData.fontSize ?? 0.33) * 100)}%)</Label> 68 - <div class="flex items-center gap-2"> 69 - <Button 70 - variant="ghost" 71 - size="sm" 72 - onclick={() => { 73 - item.cardData.fontSize = Math.max((item.cardData.fontSize ?? 0.33) - 0.05, 0.1); 74 - }} 75 - disabled={(item.cardData.fontSize ?? 0.33) <= 0.1} 76 - > 77 - <svg 78 - xmlns="http://www.w3.org/2000/svg" 79 - width="16" 80 - height="16" 81 - viewBox="0 0 24 24" 82 - fill="none" 83 - stroke="currentColor" 84 - stroke-width="2" 85 - stroke-linecap="round" 86 - stroke-linejoin="round" 87 - > 88 - <path d="M5 12h14" /> 89 - </svg> 90 - </Button> 91 - <input 92 - type="range" 93 - min="0.1" 94 - max="0.8" 95 - step="0.01" 96 - value={item.cardData.fontSize ?? 0.33} 97 - oninput={(e) => { 98 - item.cardData.fontSize = parseFloat(e.currentTarget.value); 99 - }} 100 - class="h-2 w-full cursor-pointer appearance-none rounded-lg bg-base-200 dark:bg-base-700" 101 - /> 102 - <Button 103 - variant="ghost" 104 - size="sm" 105 - onclick={() => { 106 - item.cardData.fontSize = Math.min((item.cardData.fontSize ?? 0.33) + 0.05, 0.8); 107 - }} 108 - disabled={(item.cardData.fontSize ?? 0.33) >= 0.8} 109 - > 110 - <svg 111 - xmlns="http://www.w3.org/2000/svg" 112 - width="16" 113 - height="16" 114 - viewBox="0 0 24 24" 115 - fill="none" 116 - stroke="currentColor" 117 - stroke-width="2" 118 - stroke-linecap="round" 119 - stroke-linejoin="round" 120 - > 121 - <path d="M5 12h14" /> 122 - <path d="M12 5v14" /> 123 - </svg> 124 - </Button> 125 - </div> 33 + class="bg-base-200 dark:bg-base-700 h-2 w-full cursor-pointer appearance-none rounded-lg" 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 - text: '', 13 - fontWeight: '900', 14 - fontFamily: 'Arial', 15 - fontSize: 0.33 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 + allowSetColor: false, 26 24 minW: 2, 27 25 minH: 2 28 26 } as CardDefinition & { type: 'fluid-text' };