You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 00:47:05