防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。

玩一点微广 11 周年的 Demo

我们组在 7 月 3 日上线了微信广告 11 周年的 H5 活动。和 去年的活动 类似:输入自己的话、浏览大家的话。由于我在上线当天休假,加之开发时间有限,没有太好地打磨和完成。整个活动很简单,服务的人也很少,但机会难得,我还是希望抽出一些时间再来玩一玩。

很多次地思考 过:

最佳实践源于对自己实现方式的追求。 它在对自我的不断试错和优化中形成。

一个设计实现的背后,可能对应的是一类交互形式。对我来说,还原设计稿、完成设计团队的任务是基础要求,我自己不满足于此,意义也不来源于此 —— 我花时间精进自己。至于能服务多少人,我决定不了。而且就是因为服务的人少,我才更希望玩一玩。

ElevenLabs 文字转盘

“转盘”这个东西,从 23 年24 年,再到这一次的活动,真是一个我不断思考最佳实践的例子了。这次的文字转盘很简单,我想在其基础上玩一个类似 elevenlabs 的效果 —— 越是远离圆心的文字,延迟越大。整体文字像个八爪鱼一样。

虽然这个效果并不适合活动,因为它会让浏览的体验变得打扰、低效,甚至本身都显得多余。但 —— 正如我所说 —— 我只是想玩一下,以增加一些经验。

之前用 react-springuseTrail 实现过类似的效果,但考虑到开源维护力度已越来越弱,这次单纯用 motion/react 实现。往后应该会逐渐弃用。

最终效果:

上下滑动体验
广
广
广
广
广
广
广
广
广

单个转盘

首先我们将整个转盘看作一个整体,它由一个 MotionValue 驱动。一个基础的框架:

const rotate = useMotionValue(0)
 
<motion.div
  drag
  {/* 拖动开始时,记录当前的旋转角度,并停止动画 */}
  onDragStart={...}
  {/* 拖动过程中,实时更新旋转角度 */}
  onDrag={...}
  {/* 拖动结束时,惯性滚动,最终结果为 power * velocity,并定位到最接近的旋转整数角度 */}
  onDragEnd={...}
  {/* 以下参数禁用自身的移动 */}
  dragMomentum={false}
  dragElastic={0}
  dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
/>
上下滑动体验
H
H
C
R
T
E
A
B
V
M
S
T
S
I
u
o
a
o
o
e
r
i
o
m
i
m
n
g
r
t
c
r
r
c
a
d
p
a
n
a
t
e
s
k
n
i
a
s
e
p
l
k
l
e
e
p
a
e
r
s
o
i
l
l
l
n
e
s
u
d
d
y
n
i
s
p
r
r
o
m
w
g
g
a
n
t
e
i
g
r
u
o
h
t
a
u
g
r
c
a
i
m
w
o
i
u
m
a
t
o
c
l
n
a
d
z
b
e
o
r
p
i
b
l
g
f
r
z
a
f
m
e
i
n
r
o
i
n
m
i
,
p
l
o
v
c
e
a
p
l
l
i
u
n
o
o
b
e
a
m
a
i
o
l
n
s
g
s
w
o
i
l
l
a
m
n
u
g
i
t
e
r
l
s
t
g
d
c
b
a
r
e
i
a
i
l
s
y
c
-
n
i
c
b
y
i
c
u
w
g
r
y
r
a
p
e
s
p
b
e
t
a
l
o
n
o
p
a
o
o
n
m
n
e

加入声音

rotate 靠近最接近的整数角度时,播放 audio。此时记录当前的旋转角度,以防止重复播放。

const texts = ["Huge epic braam", ...]
const rotate = useMotionValue(0)
const lastAudioPlayedDeg = useRef(0)
 
rotate.on("change", (value) => {
  const nearest = Math.round(value / (360 / texts.length)) * (360 / texts.length)
 
  if (Math.abs(nearest - value) < 3 && nearest !== lastAudioPlayedDeg.current) {
    if (audioRef.current) {
      audioRef.current.currentTime = 0
      audioRef.current.play()
    }
    lastAudioPlayedDeg.current = nearest
  }
})
 
<audio ref={audioRef} />
上下滑动体验
H
H
C
R
T
E
A
B
V
M
S
T
S
I
u
o
a
o
o
e
r
i
o
m
i
m
n
g
r
t
c
r
r
c
a
d
p
a
n
a
t
e
s
k
n
i
a
s
e
p
l
k
l
e
e
p
a
e
r
s
o
i
l
l
l
n
e
s
u
d
d
y
n
i
s
p
r
r
o
m
w
g
g
a
n
t
e
i
g
r
u
o
h
t
a
u
g
r
c
a
i
m
w
o
i
u
m
a
t
o
c
l
n
a
d
z
b
e
o
r
p
i
b
l
g
f
r
z
a
f
m
e
i
n
r
o
i
n
m
i
,
p
l
o
v
c
e
a
p
l
l
i
u
n
o
o
b
e
a
m
a
i
o
l
n
s
g
s
w
o
i
l
l
a
m
n
u
g
i
t
e
r
l
s
t
g
d
c
b
a
r
e
i
a
i
l
s
y
c
-
n
i
c
b
y
i
c
u
w
g
r
y
r
a
p
e
s
p
b
e
t
a
l
o
n
o
p
a
o
o
n
m
n
e

多个转盘

elevenlabs 是通过每一个字符分别运动实现的,但这可以优化一下。

既然字符是 mono 等宽的,我们可以把每一行中相同 index 的字符看作一个圆。这样一层一层往外,形成多个同心圆。对于上面的文字我们只需要 23 个转盘,因为最长的文字 "Small automobile weapon" 的长度为 23,页面上只会有 23 个元素运动。而 elevenlabs 的元素数量则需要 23 * 14 = 322 个。

function splitByCharacter(arr: string[]) {
  // 找到最大字符串长度
  const maxLength = Math.max(...arr.map((text) => text.length))
  const result = []
  for (let i = 0; i < maxLength; i++) {
    // 获取每个字符串第 i 个字符,若超出则用空字符串代替
    const tempArr = arr.map((text) => text[i] || "")
    result.push(tempArr)
  }
  return result
}

每一个转盘都接收外层的 rotate 作为参数,在组件内部通过 motion/react 提供的非常方便的 useSpring 将其转换为不同 visualDurationMotionValue

{
  splitTexts.map((text, index) => (
    <CircularText key={index} rotate={rotate} texts={text} charIndex={index} />
  ))
}
 
const CircularText = ({ rotate }) => {
  const rotate = useSpring(rotateProp, {
    visualDuration: step * charIndex,
    bounce: 0,
    restDelta: 0.01,
  })
}
上下滑动体验
广
广
广
广
广
广
广
广
广

我们的文字转盘

我们的文字允许多行,且不等宽:

上下滑动体验
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!

因此要加上类似 elevenlabs 的效果就会变得复杂,必须让所有字符单独运动,关键是最终效果确实不一定好。

简单起见,一开始我通过 useVelocity 让字符整体做一个 skew 变换,但文字被扭曲的效果并不好,或者说是另一种感觉,我们就此决定放弃:

// 如果要用 useVelocity,就要把 useMotionValue 替换成 useSpring,否则 velocity 容易出现跳变
// const rotateValue = useMotionValue(0)
const rotateValue = useSpring(0, {
  visualDuration: 0.2,
  bounce: 0,
  restDelta: 0.001,
})
const rotateValueVelocity = useVelocity(rotateValue)
 
const skewYValue = useTransform(rotateValueVelocity, (v) => -v / 20)
const rotateFinal = useTransform(rotateValueVelocity, (v) => -v / 25)
 
const velocityTransform = useTransform(
  [rotateFinal, skewYValue],
  ([rotate, skewY]) =>
    `translate3d(0, 0, 0) rotate(${rotate}deg) skewY(${skewY}deg)`
)
上下滑动体验
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!
自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
微广十一周年生日快乐
超级喜欢和大家一起并肩作战的感觉!
祝微信广告越来越好!

那我现在就尝试一下让每个字符单独运动。首先每一个字符的容器需要分开,除了字符还需要考虑左右两个棒棒,它们也需要给到不同的 visualDuration

实时拿到每个字符距离容器右侧的值,越是靠右的元素,visualDuration 越小;越是靠左的元素,visualDuration 越大。也就是说左边的棒棒是最慢的,右边的棒棒是最快的:

useEffect(() => {
  if (charRef.current) {
    // 拿到 charRef.current 距离容器右边多少
    const rect = charRef.current.getBoundingClientRect()
    const wrapperRect = wrapperRef.current.getBoundingClientRect()
    const duration = 0.01 * (wrapperRect.right - rect.right) * 0.07
    setVisualDuration(duration)
  }
}, [charRef.current])

这样,原先一整个转盘的动画,一下就变成了几百个元素的动画,性能一定存在问题了。Anyway, it's fun。效果如下:

上下滑动体验
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广
广

点赞动画

点赞如果只是数字的增加就太没有氛围了,我们可以把两根应援棒变成烟花棒玩玩。你应该也在首页见到了:

自强不息,厚德载物。祝微信广告十一周年生日快乐,越来越好。
Aragakey.
11

更多

还有一些也值得尝试,但我精力有限。

比如我们在用户输入的时候希望能够让 textarea 的字体大小是动态调节的,随着输入字符的增多,字体大小从 30px 流动变换到 24px,再比如对 textarea 多行逻辑的判断等。这些都是我没有尝试过的 UI。因为时间有限,我们也没有太深入打磨。

另外入场动画第一次使用 motion/react 实现,API 很方便。但 JS 动画确实存在性能顾虑,总是让我不放心,但小活动嘛就用了。

总之,有机会再玩!

欢迎与我交流: