如何实现SVG textPath元素的平滑动画?(Svelte场景)
问题
已实现沿圆形外侧路径排列的文字,点击next按钮时圆形会随机改变尺寸,目前圆形和路径已通过CSS过渡实现平滑动画,但<text>/<textPath>元素无法直接用CSS过渡实现动画效果,需要可行的解决方案。
原Svelte代码示例:
<script> let r = 100 let bez = 4*(Math.sqrt(2)-1)/3*r let cx = 250 let cy = 200 function next() { r = Math.random() *100 bez = 4*(Math.sqrt(2)-1)/3*r } </script> <svg> <g> <circle cx="{cx}" cy="{cy}" r="{r*0.9}" opacity="0.05"></circle> <path stroke="cornflowerblue" stroke-dasharray="4" fill="none" d="M{cx},{cy-r} C{cx+bez},{cy-r} {cx+r},{cy-bez} {cx+r},{cy} C{cx+r},{cy+bez} {cx+bez},{cy+r} {cx},{cy+r} C{cx-bez},{cy+r} {cx-r},{cy+bez} {cx-r},{cy} C{cx-r},{cy-bez} {cx-bez},{cy-r} {cx},{cy-r}" id="circle-text"></path> <text fill="red"> <textPath href="#circle-text" > Lorem Ipsum Dolor Sit Amet </textPath> </text> </g> </svg> <button on:click={next}> next </button> <style> svg { width:500px; height:400px; background-color:lightgrey; } circle { transition:all 500ms ease-in-out; } path { transition:all 500ms ease-in-out; } text { transition:all 500ms ease-in-out; } textPath { transition:all 500ms ease-in-out; } </style>
解决方案1:双textPath交叉淡入淡出
核心思路是同时保留新旧两组路径与文本元素,通过过渡透明度实现平滑切换,动画完成后清除旧元素。
修改后的Svelte代码:
<script> import { tweened } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; let cx = 250; let cy = 200; let currentR = tweened(100, { duration: 500, easing: cubicOut }); let oldPath = null; let showOldText = false; $: bez = 4*(Math.sqrt(2)-1)/3 * $currentR; $: pathD = `M${cx},${cy-$currentR} C${cx+bez},${cy-$currentR} ${cx+$currentR},${cy-bez} ${cx+$currentR},${cy} C${cx+$currentR},${cy+bez} ${cx+bez},${cy+$currentR} ${cx},${cy+$currentR} C${cx-bez},${cy+$currentR} ${cx-$currentR},${cy+bez} ${cx-$currentR},${cy} C${cx-$currentR},${cy-bez} ${cx-bez},${cy-$currentR} ${cx},${cy-$currentR}`; function next() { // 保存当前路径,显示旧文本 oldPath = pathD; showOldText = true; // 触发新半径动画 currentR.set(Math.random() * 100); // 动画结束后隐藏旧文本 setTimeout(() => showOldText = false, 500); } </script> <svg> <g> <circle cx="{cx}" cy="{cy}" r="{$currentR*0.9}" opacity="0.05"></circle> <!-- 旧路径与文本(过渡期间显示) --> {#if showOldText} <path stroke="cornflowerblue" stroke-dasharray="4" fill="none" d={oldPath} opacity="0" style="transition: opacity 500ms ease-in-out;"> </path> <text fill="red" opacity="0" style="transition: opacity 500ms ease-in-out;"> <textPath href="#old-circle-text"> Lorem Ipsum Dolor Sit Amet </textPath> </text> {/if} <!-- 新路径与文本 --> <path stroke="cornflowerblue" stroke-dasharray="4" fill="none" d={pathD} id="circle-text"> </path> <text fill="red"> <textPath href="#circle-text"> Lorem Ipsum Dolor Sit Amet </textPath> </text> </g> </svg> <button on:click={next}> next </button> <style> svg { width:500px; height:400px; background-color:lightgrey; } circle { transition: all 500ms ease-in-out; } path { transition: all 500ms ease-in-out; } </style>
解决方案2:通过tweened控制文本缩放与位置
利用Svelte的tweened存储变量,同步过渡文本的transform属性(缩放+位移),配合路径动画模拟文本随圆形平滑变化的效果。
代码示例:
<script> import { tweened } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; let cx = 250; let cy = 200; let r = tweened(100, { duration: 500, easing: cubicOut }); $: bez = 4*(Math.sqrt(2)-1)/3 * $r; $: pathD = `M${cx},${cy-$r} C${cx+bez},${cy-$r} ${cx+$r},${cy-bez} ${cx+$r},${cy} C${cx+$r},${cy+bez} ${cx+bez},${cy+$r} ${cx},${cy+$r} C${cx-bez},${cy+$r} ${cx-$r},${cy+bez} ${cx-$r},${cy} C${cx-$r},${cy-bez} ${cx-bez},${cy-$r} ${cx},${cy-$r}`; // 基于当前半径与初始半径的比值计算缩放比例 $: scale = $r / 100; function next() { r.set(Math.random() * 100); } </script> <svg> <g> <circle cx="{cx}" cy="{cy}" r="{$r*0.9}" opacity="0.05"></circle> <path stroke="cornflowerblue" stroke-dasharray="4" fill="none" d={pathD} id="circle-text"> </path> <text fill="red" transform={`translate(${cx}, ${cy}) scale(${scale}) translate(${-cx}, ${-cy})`} style="transition: transform 500ms ease-in-out;"> <textPath href="#circle-text"> Lorem Ipsum Dolor Sit Amet </textPath> </text> </g> </svg> <button on:click={next}> next </button> <style> svg { width:500px; height:400px; background-color:lightgrey; } circle { transition: all 500ms ease-in-out; } path { transition: all 500ms ease-in-out; } </style>
解决方案3:动态调整textPath的startOffset
通过计算路径长度变化,同步调整textPath的startOffset属性,让文本在路径过渡时保持相对位置平滑移动。
代码示例:
<script> import { tweened } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; let cx = 250; let cy = 200; let r = tweened(100, { duration: 500, easing: cubicOut }); let pathElement; let prevPathLength = 2 * Math.PI * 100; // 初始圆形路径长度 $: bez = 4*(Math.sqrt(2)-1)/3 * $r; $: pathD = `M${cx},${cy-$r} C${cx+bez},${cy-$r} ${cx+$r},${cy-bez} ${cx+$r},${cy} C${cx+$r},${cy+bez} ${cx+bez},${cy+$r} ${cx},${cy+$r} C${cx-bez},${cy+$r} ${cx-$r},${cy+bez} ${cx-$r},${cy} C${cx-$r},${cy-bez} ${cx-bez},${cy-$r} ${cx},${cy-$r}`; $: { if (pathElement) { const currentPathLength = pathElement.getTotalLength(); // 按路径长度比例调整startOffset,保持文本位置一致 const textPath = document.querySelector('textPath'); if (textPath) { const currentOffset = parseFloat(textPath.getAttribute('startOffset')) || 0; textPath.setAttribute('startOffset', `${currentOffset * (prevPathLength / currentPathLength)}%`); } prevPathLength = currentPathLength; } } function next() { r.set(Math.random() * 100); } </script> <svg> <g> <circle cx="{cx}" cy="{cy}" r="{$r*0.9}" opacity="0.05"></circle> <path bind:this={pathElement} stroke="cornflowerblue" stroke-dasharray="4" fill="none" d={pathD} id="circle-text"> </path> <text fill="red"> <textPath href="#circle-text" startOffset="0%" style="transition: startOffset 500ms ease-in-out;"> Lorem Ipsum Dolor Sit Amet </textPath> </text> </g> </svg> <button on:click={next}> next </button> <style> svg { width:500px; height:400px; background-color:lightgrey; } circle { transition: all 500ms ease-in-out; } path { transition: all 500ms ease-in-out; } </style>
内容的提问来源于stack exchange,提问作者louislugas
相关产品推荐
相关产品推荐

