在多个节点之间过渡单个节点的渐变填充
Transitioning the gradient fill of a single node among many
假设我有一个结构类似于这样的 SVG:
<svg>
<defs>
<linearGradient id="gradient-red">...</linearGradient>
<linearGradient id="gradient-blue">...</linearGradient>
</defs>
<g class="node">
<circle r="50" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="100" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="150" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="200" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="250" style="fill: url('#gradient-red');"></circle>
</g>
</svg>
我现在有五个带红色渐变的圆圈。我了解如何更改选定节点的颜色——我只是将其作为目标(通过 d3.select
)并将其样式更改为 'fill', 'url("#gradient-blue")
。但是我将如何过渡那个节点的渐变填充从红色到蓝色?
这样的事情不会导致 tween/transition 而是会导致即时颜色交换:
d3.transition().duration(1000)
.tween('start', () => {
let test = d3.select(currentTarget);
test.transition().duration(1000).style('fill', 'url("#gradient-blue")');
如果我要转换渐变本身的 stop-color
,它会改变 all nodes/circles(因为你正在改变 <defs>
)。
我做错了什么?
过渡的插值
在 D3 中,转换基本上是将起始值插入到结束值。如果我们对数字进行插值,这很容易证明。例如,让我们从 50
过渡到 2000
:
const interpolator = d3.interpolate(50, 2000);
d3.range(0, 1.05, 0.05).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
我们也可以插入字符串:
const interpolator = d3.interpolate("March, 2000", "March, 2020");
d3.range(0, 1.05, 0.05).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
问题
现在,让我们看一下您的情况:您想从中进行插值:
url("#gradient-red")
为此:
url("#gradient-blue")
这里可能的中间体是什么?你能看出这是不可能的吗?这是证明:
const interpolator = d3.interpolate("url(#gradient-red)", "url(#gradient-blue)");
d3.range(0, 1.1, 0.1).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
如您所见,第一个插值会立即导致最终值。
可能的解决方案
最明显的解决方案是插入停止色。但是,正如您刚刚发现的那样,这将改变 所有 个圆的渐变。
因此,天真的解决方法是创建多个渐变,每个圆一个,具有唯一 ID。虽然这对于 3 或 4 个圆圈来说可能是一个合适的解决方案,但如果您有数十个或数百个元素,这显然不是一个聪明的解决方案。
话虽如此,这是我的建议:
- 创建一个临时渐变,让我们给它ID
#gradient-temporary
,就像红色的一样。
- 然后,当您 select(或以某种方式过滤它)一个圆时,将其填充从
"url(#gradient-red)"
更改为 "url(#gradient-temporary)"
。这种变化是立竿见影的,在屏幕上没有明显的影响。
- 在此临时渐变的停止颜色上进行过渡。
- 过渡完成后,将圆的填充从
"url(#gradient-temporary)"
更改为 "url(#gradient-blue)"
。同样,这是即时的。另外,将临时渐变的终止色改回红色。
这样,你可以有数百个圆圈,但你只需要 3 个渐变来过渡它们。
这是一个使用该方法的演示,单击每个圆圈进行转换:
const circles = d3.selectAll("circle");
circles.on("click", function() {
const element = this;
d3.select(element).style("fill", "url(#gradient-temporary)");
d3.select("#gradient-temporary").select("stop:nth-child(2)")
.transition()
.duration(1000)
.style("stop-color", "rgb(0,0,255)")
.on("end", function() {
d3.select(element).style("fill", "url(#gradient-blue)");
d3.select("#gradient-temporary").select("stop:nth-child(2)")
.style("stop-color", "rgb(255,0,0)")
})
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<defs>
<linearGradient id="gradient-red" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-temporary" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-blue" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(0,0,255);stop-opacity:1" />
</linearGradient>
</defs>
<g class="node">
<circle r="20" cx="20" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="80" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="140" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="200" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="260" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
</svg>
假设我有一个结构类似于这样的 SVG:
<svg>
<defs>
<linearGradient id="gradient-red">...</linearGradient>
<linearGradient id="gradient-blue">...</linearGradient>
</defs>
<g class="node">
<circle r="50" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="100" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="150" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="200" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="250" style="fill: url('#gradient-red');"></circle>
</g>
</svg>
我现在有五个带红色渐变的圆圈。我了解如何更改选定节点的颜色——我只是将其作为目标(通过 d3.select
)并将其样式更改为 'fill', 'url("#gradient-blue")
。但是我将如何过渡那个节点的渐变填充从红色到蓝色?
这样的事情不会导致 tween/transition 而是会导致即时颜色交换:
d3.transition().duration(1000)
.tween('start', () => {
let test = d3.select(currentTarget);
test.transition().duration(1000).style('fill', 'url("#gradient-blue")');
如果我要转换渐变本身的 stop-color
,它会改变 all nodes/circles(因为你正在改变 <defs>
)。
我做错了什么?
过渡的插值
在 D3 中,转换基本上是将起始值插入到结束值。如果我们对数字进行插值,这很容易证明。例如,让我们从 50
过渡到 2000
:
const interpolator = d3.interpolate(50, 2000);
d3.range(0, 1.05, 0.05).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
我们也可以插入字符串:
const interpolator = d3.interpolate("March, 2000", "March, 2020");
d3.range(0, 1.05, 0.05).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
问题
现在,让我们看一下您的情况:您想从中进行插值:
url("#gradient-red")
为此:
url("#gradient-blue")
这里可能的中间体是什么?你能看出这是不可能的吗?这是证明:
const interpolator = d3.interpolate("url(#gradient-red)", "url(#gradient-blue)");
d3.range(0, 1.1, 0.1).forEach(function(d) {
console.log(interpolator(d))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
如您所见,第一个插值会立即导致最终值。
可能的解决方案
最明显的解决方案是插入停止色。但是,正如您刚刚发现的那样,这将改变 所有 个圆的渐变。
因此,天真的解决方法是创建多个渐变,每个圆一个,具有唯一 ID。虽然这对于 3 或 4 个圆圈来说可能是一个合适的解决方案,但如果您有数十个或数百个元素,这显然不是一个聪明的解决方案。
话虽如此,这是我的建议:
- 创建一个临时渐变,让我们给它ID
#gradient-temporary
,就像红色的一样。 - 然后,当您 select(或以某种方式过滤它)一个圆时,将其填充从
"url(#gradient-red)"
更改为"url(#gradient-temporary)"
。这种变化是立竿见影的,在屏幕上没有明显的影响。 - 在此临时渐变的停止颜色上进行过渡。
- 过渡完成后,将圆的填充从
"url(#gradient-temporary)"
更改为"url(#gradient-blue)"
。同样,这是即时的。另外,将临时渐变的终止色改回红色。
这样,你可以有数百个圆圈,但你只需要 3 个渐变来过渡它们。
这是一个使用该方法的演示,单击每个圆圈进行转换:
const circles = d3.selectAll("circle");
circles.on("click", function() {
const element = this;
d3.select(element).style("fill", "url(#gradient-temporary)");
d3.select("#gradient-temporary").select("stop:nth-child(2)")
.transition()
.duration(1000)
.style("stop-color", "rgb(0,0,255)")
.on("end", function() {
d3.select(element).style("fill", "url(#gradient-blue)");
d3.select("#gradient-temporary").select("stop:nth-child(2)")
.style("stop-color", "rgb(255,0,0)")
})
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<defs>
<linearGradient id="gradient-red" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-temporary" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1" />
</linearGradient>
<linearGradient id="gradient-blue" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(211,211,211);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(0,0,255);stop-opacity:1" />
</linearGradient>
</defs>
<g class="node">
<circle r="20" cx="20" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="80" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="140" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="200" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
<g class="node">
<circle r="20" cx="260" cy="70" style="fill: url('#gradient-red');"></circle>
</g>
</svg>