惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

Anthony Fu

Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory (React ver.) Mental Health in Open Source The Evolution of Shiki v1.0 Anthony's Roads to Open Source - The Progressive Path Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory Now, and the Future of Nuxt Devtools Anthony's Roads to Open Source - The Set Theory Anthony Fu Anthony Fu Stable Diffusion QR Code 101 Refining AI Generated QR Code Stylistic QR Code with Stable Diffusion Anthony Fu How I Manage GitHub Notifications Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Dev SSR on Nuxt with Vite Why I don't use Prettier Anthony Fu Anthony Fu Ship ESM & CJS in one Package Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Reflection of Speaking in Public Anthony Fu Windi CSS and Tailwind JIT Typed Provide and Inject in Vue Color Scheme for VS Code Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Destructuring... with object or array? Anthony Fu Anthony Fu Make Libraries Working with Vue 2 and 3 Anthony Fu Anthony Fu Anthony Fu Anthony Fu
The Magic in Shiki Magic Move
Anthony Fu · 2024-03-04 · via Anthony Fu
  • How it works
  • Transitions
    • Enter Transition
    • Leave Transition
    • Move Transition
  • Integrations

Magic Move is a feature for transition in Keynote, or it’s called Morph Transition in PowerPoint, and it automatically animates the transitions for objects between slides. It is quite impressive, very intuitive and effortless to use, and can be applied to various types of objects. Like you can paste a highlighted code block, or make another in a second slide, Magic Move will do the transition as granular to the tokens level for you.

The only annoying part of this process is that Keynote does not support code highlighting - so you need to highlight the code somewhere and paste them manually every single time. This is one of the reasons I made Slidev - to have first-class tooling for developers to make presentations easier. While moving to web technologies with Slidev opens up almost infinite possibilities, on the other hand, it also makes some nice cool features in Keynote and PowerPoint harder to achieve (you need to write quite some extra code) - for example, the Magic Move.

Browsers’ new View Transitions API makes morphing between elements a lot easier, only that it requires some manual work to assign keys to make the pairs. While it’s rather ok to do for a few big elements, doing such manually for every single token in a code block is basically unacceptable.

Roughly a year ago, Eduardo @posva and I came up with the idea of using Shiki combined with Vue’s <TransitionGroup> to achieve a similar effect for code blocks:

We managed to make the proof of concept work, as shown in the video. However, due to some hard edge cases, both of us were busy with other things, and we didn’t manage to make it a usable library at that time. It has come to our discussions from time to time but we didn’t come up with a good solution. Until one day, a few weeks ago while I was preparing my slides, my productive procrastination kicked in and I decided to give it another try to escape from my slides.

And luck me, I found a quite nice approach and made it happen:

Made a playground where you can try it out yourself, and you can find the source code at shikijs/shiki-magic-move.

So today here, let’s break it down and see how it works.

How it works

To get started, we could see each word in the code with different colors as a token (they are <span> elements in practice). Basically, we can consider them as a set of small objects that we want to animate individually.

To break done the animations of Magic Move, we see we are basically trying to find connections between two sets of objects and animate them from one to another. Different from 1-to-1 transitions, here we expect the code before and after to be different, which means we could categorize the type of transitions into 3 categories:

  1. Move: The token exists in both before and after, so we just need to move it to the new position.
  2. Enter: The token only exists in the after, we need to animate the element entering the scene.
  3. Leave: The token only exists in the before, we need to animate the element leaving the scene.

Here is a playground for you to inspect the type of each token, hover over to inspect:

In the playground above, we assign a key to each token. Move tokens come with pairs, so we assign the same key to make the "connection".

When doing highlighting, Shiki turns the input code into an array of tokens. We can run Shiki twice for code before and after to get two collections of tokens. It should be fairly simple to find the Enter and Leave tokens by running two loops to compare the two collections. However, the challenge is to find good pairs of Move tokens. If we only pair them by the content of each token, it would be the case that 1-to-many or many-to-1 might make the pair transition off.

I came up with the idea of using a text diff algorithm to find the chunks of the code that are matched between the two versions. I ended up using Google’s Diff Match Patch (I later refactored it into ESM as diff-match-patch-es) to achieve this. With the diff result, we can now reliably find the pairs of the Move tokens without worrying that tokens might travel to the wrong place.

With this core logic in place, we can generate the correct keys for each token for the connection. This made the rest of the task clear, we just needed to apply different transitions to different types of tokens. We could feed them into any key-based transition library, for example, Vue provides a built-in <TransitionGroup> component that does the job perfectly, live example.

Transitions

While Vue’s <TransitionGroup> should get the transition done automatically, it’s actually a bit tricky to do in our specific case. The main reason is that the token elements in the code are <span> that rely on browsers’ layout engine to calculate the position. During the transition, we want to make each token positioned absolutely to avoid messing up the layout. This means we need to record the position of each token with getBoundingClientRect() before the transition starts, apply the absolute position to them during transitions, and then restore to the inline layout after the transition ends.

Ran into some limitations of <TransitionGroup> and also with the wish to have this a framework-agnostic solution, I ended up writing a custom renderer to do that, referencing a lot of the code from <TransitionGroup>.

Because we are relying on the browsers’ layout engine to calculate the position, we need to get the destination position of each token (the position of the final code) before the transition starts. I found this trick in Vue’s code to force layout reflow combined with temporary setting transition duration to 0 to get the new position immediately so we could start animating.

Then it comes to do the transitions for different types of tokens:

Enter Transition

The enter transition is the most straightforward one. Because the token will stay at the destination position after the transition, we don’t need to do anything with the positioning. We usually just need to apply the opacity transition to make it appear. Here we add/remove classes for users to apply the transition with CSS.

Pseudo-code below:

for (const el of enterElements) {
  el.classList.add('transition-enter')
  el.classList.add('transition-enter-from')
}

// Replace the children of the container with
// elements from the new code
container.replaceChildren(...newChildren)
// Force layout reflow
forceReflow()

for (const el of enterElements) {
  el.classList.remove('transition-enter-from')
  el.classList.add('transition-enter-to')
}

// Here the transition starts
// from `.transition-enter-from` to `.transition-enter-to`

for (const el of enterElements) {
  // Transition Finished
  el.addEventListener('transitionend', () => {
    el.classList.remove('transition-enter-to')
  })
}

[actual source code]

Leave Transition

Since "Leave" tokens eventually disappear after the transition, we need to keep them in the DOM tree for a while for animations but we don’t want them to participate in the layout. We can apply position: absolute to them and set the top and left to the original position to make them stay in place. Then we can apply the opacity transition to make them disappear.

Pseudo-code below:

for (const el of leaveElements) {
  // Get the position of the token stored before
  const pos = position.get(el)!
  // Set absolute position
  el.style.position = 'absolute'
  el.style.top = `${pos.y}px`
  el.style.left = `${pos.x}px`

  el.classList.add('transition-leave')
  el.classList.add('transition-leave-from')
}

// Replace the children of the container
// Same as the enter transition
container.replaceChildren(...newChildren)
forceReflow()

for (const el of enterElements) {
  el.classList.remove('transition-leave-from')
  el.classList.add('transition-leave-to')
}

for (const el of enterElements) {
  el.addEventListener('transitionend', () => {
    el.classList.remove('transition-leave-to')
  })
}

[actual source code]

Move Transition

To animate "Move" tokens requires a bit more work. We use a technique called "FLIP" (First, Last, Invert, Play) to make the transition smooth. We need to record the position of each token before the transition starts, then apply the absolute position to them during the transition, and then restore it to the inline layout after the transition ends.

It’s a bit unintuitive to understand at first, but luckily David Khourshid made a great explanation at Animating Layouts with the FLIP Technique, definitely worth reading!

Pseudo-code below:

for (const el of moveElements) {
  const newPos = el.getBoundingClientRect()
  const oldPos = position.get(el)!
  const dx = oldPos.x - newPos.x
  const dy = oldPos.y - newPos.y

  // Set duration to 0 to get the new position immediately
  el.style.transitionDuration = '0ms'
  el.style.transitionDelay = '0ms'
  // Transform new elements to the old position
  el.style.transform = `translate(${dx}px, ${dy}px)`
}

// Replace the children of the container
container.replaceChildren(...newChildren)
forceReflow()

for (const el of moveElements) {
  // Remove transform overrides,
  // so it will start animating back to the new position
  el.classList.add('transition-move')
  el.style.transform = ''
  el.style.transitionDuration = ''
  el.style.transitionDelay = ''
}

for (const el of moveElements) {
  el.addEventListener('transitionend', () => {
    el.classList.remove('transition-move')
  })
}

[actual source code]

Integrations

Shiki Magic Move is open-sourced at shikijs/shiki-magic-move. The core logic is pretty lightweight and framework-agnostic, despite that the library is a bit low-level. Currently, I only have the bandwidth to make a Vue wrapper for it, and I am counting on you to contribute and add more first-class integrations for other frameworks as well as higher-level integrations.

If you are using Slidev, you can try it today to enhance your slides with Magic!

Hope you enjoy it, happy hacking!