From 35f1f02370475783881d95aa8f284b60b55065cb Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Thu, 13 Jul 2023 07:54:31 +0000 Subject: [PATCH 01/24] Add experiment --- packages/mdx/pages/css-animations.js | 63 ++++++++ packages/mdx/pages/styles.css | 28 ++++ packages/mdx/src/lines.js | 210 +++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 packages/mdx/pages/css-animations.js create mode 100644 packages/mdx/src/lines.js diff --git a/packages/mdx/pages/css-animations.js b/packages/mdx/pages/css-animations.js new file mode 100644 index 00000000..806e5dae --- /dev/null +++ b/packages/mdx/pages/css-animations.js @@ -0,0 +1,63 @@ +import React from "react" +import { setLines } from "../src/lines" + +const a = [ + { content: "A1", id: "1" }, + { content: "A2", id: "2" }, + { content: "A3", id: "3" }, + { content: "A4", id: "4" }, + { content: "A5", id: "5" }, +] + +const b = [ + { content: "A1", id: "1" }, + { content: "B1", id: "1.5" }, + { content: "A2", id: "2" }, + { content: "A3", id: "3" }, + { content: "A5", id: "5" }, +] + +const c = [ + { content: "B1", id: "1.5" }, + { content: "C1", id: "1.6" }, + { content: "A3", id: "3" }, +] + +export default function Home() { + return +} + +function Code({ lines }) { + const ref = React.useRef() + console.log("render") + return ( +
+ +
+ {lines.map((line, i) => { + return ( +
+ {line.content} +
+ ) + })} +
+
+ ) +} diff --git a/packages/mdx/pages/styles.css b/packages/mdx/pages/styles.css index 2d298a81..e295d707 100644 --- a/packages/mdx/pages/styles.css +++ b/packages/mdx/pages/styles.css @@ -112,3 +112,31 @@ div#__next > div { --ch-25: #afb8c133; --ch-26: #ffffffe6; } + +.code { + background-color: #222; + padding: 20px; + font-size: 15px; + color: #fff; + width: 600px; + height: 400px; + margin: 0 auto; + padding-left: 300px; + font-family: monospace; +} + +.line { + outline: 1px dashed #9999; + width: 200px; + animation: x 1s ease-in-out; + animation-fill-mode: backwards; +} + +@keyframes x { + 0% { + transform: var(--transform); + opacity: var(--opacity); + } + 100% { + } +} diff --git a/packages/mdx/src/lines.js b/packages/mdx/src/lines.js new file mode 100644 index 00000000..34db250f --- /dev/null +++ b/packages/mdx/src/lines.js @@ -0,0 +1,210 @@ +const EXIT_DURATION = 0.5 + +export function setLines(parent, lines) { + const oldLineElements = Array.from(parent.children) + + const measureOld = measureProperties(oldLineElements) + + setFinalScene(parent, oldLineElements, lines, measureOld) + + const newLineElements = Array.from(parent.children) + + const measureNew = measureProperties(newLineElements) + + fillAnimationStart(measureOld, measureNew) +} + +function fillAnimationStart(measureOld, measureNew) { + const exits = [] + const enters = [] + const moves = [] + + measureNew.forEach((mNew, element) => { + if (mNew.transform !== "none") { + exits.push(element) + } else if (measureOld.has(element)) { + moves.push(element) + } else { + enters.push(element) + } + }) + + const exitCounts = exits.length + const exitsDuration = fullStaggerDuration(exitCounts) + + exits.forEach((element, i) => { + const mNew = measureNew.get(element) + const mOld = measureOld.get(element) + + const dx = mOld.x - mNew.x + const dy = mOld.y - mNew.y + + element.style.setProperty("--opacity", mOld.opacity) + element.style.setProperty( + "--transform", + `${mNew.transform} translateX(${dx}px) translateY(${dy}px)` + ) + element.style.setProperty("animation-name", "x") + element.style.setProperty( + "animation-delay", + `${staggerDelay(i, exitCounts, exitsDuration)}s` + ) + }) + + moves.forEach(element => { + const mNew = measureNew.get(element) + const mOld = measureOld.get(element) + + const dx = mOld.x - mNew.x + const dy = mOld.y - mNew.y + + element.style.setProperty("--opacity", mOld.opacity) + element.style.setProperty( + "--transform", + `translateX(${dx}px) translateY(${dy}px)` + ) + element.style.setProperty("animation-name", "x") + element.style.setProperty( + "animation-delay", + `${exitsDuration}s` + ) + }) + + const enterStart = + exitsDuration + (moves.length > 0 ? EXIT_DURATION : 0) + const enterCounts = enters.length + const entersDuration = fullStaggerDuration(enterCounts) + + enters.forEach((element, i) => { + const delay = + enterStart + + staggerDelay(i, enterCounts, entersDuration) + console.log({ + enterStart, + enterCounts, + entersDuration, + delay, + }) + element.style.setProperty("--opacity", "0.1") + element.style.setProperty( + "--transform", + "translateX(100%)" + ) + element.style.setProperty("animation-name", "x") + element.style.setProperty( + "animation-delay", + `${delay}s` + ) + }) +} + +function measureProperties(oldLineElements) { + const measures = new Map() + for (const line of oldLineElements) { + const id = line.getAttribute("data-ch-lid") + const style = window.getComputedStyle(line) + const rect = line.getBoundingClientRect() + measures.set(line, { + id, + opacity: style.opacity, + transform: style.transform, + animationName: style.animationName, + y: rect.y, + x: rect.x, + }) + } + return measures +} + +function setFinalScene( + parent, + lineElements, + lines, + measureOld +) { + let oldIndex = 0 + let newIndex = 0 + + const oldIds = [...measureOld.values()].map(m => m.id) + const newIds = lines.map(line => line.id) + + const toBeRemoved = [] + + while ( + oldIndex < lineElements.length || + newIndex < lines.length + ) { + const oldElement = lineElements[oldIndex] + const newLine = lines[newIndex] + const m = measureOld.get(oldElement) + const oldId = m?.id + const newId = newLine?.id + + if (newId && !oldIds.includes(newId)) { + addLine(parent, newLine, oldElement) + newIndex++ + continue + } + + oldElement.style.setProperty("animation-name", "none") + + // TODO change opacity to 0 + if (!oldId && m.opacity === "0.1") { + oldElement.remove() + oldIndex++ + continue + } + + if (!newIds.includes(oldId)) { + prepareToRemoveLine(oldElement) + toBeRemoved.push(oldElement) + oldIndex++ + continue + } + + oldIndex++ + newIndex++ + } + + // now that we have all the lines, we need to find the transformY for the absolute positioned lines + const dys = toBeRemoved.map(element => { + const oldY = measureOld.get(element).y + const newY = element.getBoundingClientRect().y + return oldY - newY + }) + + toBeRemoved.forEach((element, i) => { + const dy = dys[i] + element.style.setProperty( + "transform", + `translateX(-100%) translateY(${dy}px)` + ) + }) +} + +function addLine(parent, newLine, oldElement) { + const newElement = document.createElement("div") + newElement.classList.add("line") + newElement.setAttribute("data-ch-lid", newLine.id) + newElement.textContent = newLine.content + parent.insertBefore(newElement, oldElement) +} + +function prepareToRemoveLine(oldElement) { + // TODO change opacity to 0 + oldElement.style.setProperty("opacity", "0.1") + oldElement.style.setProperty("position", "absolute") + oldElement.dataset.chLid = "" +} + +function fullStaggerDuration(count) { + if (count === 0) return 0 + return 2 * EXIT_DURATION * (1 - 1 / (1 + count)) +} + +function staggerDelay(i, n, duration) { + if (i === 0) return 0 + const max = duration - EXIT_DURATION + console.log({ i, n, max }) + return (i / (n - 1)) * max +} From be70218af9ddd1e24adae519644f5f0c7aadf7b7 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Sat, 29 Jul 2023 13:14:51 +0200 Subject: [PATCH 02/24] Duration var --- packages/mdx/pages/styles.css | 2 +- packages/mdx/src/lines.js | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/mdx/pages/styles.css b/packages/mdx/pages/styles.css index e295d707..1d414a07 100644 --- a/packages/mdx/pages/styles.css +++ b/packages/mdx/pages/styles.css @@ -128,7 +128,7 @@ div#__next > div { .line { outline: 1px dashed #9999; width: 200px; - animation: x 1s ease-in-out; + animation: x var(--duration) ease-in-out; animation-fill-mode: backwards; } diff --git a/packages/mdx/src/lines.js b/packages/mdx/src/lines.js index 34db250f..e2ede18f 100644 --- a/packages/mdx/src/lines.js +++ b/packages/mdx/src/lines.js @@ -1,4 +1,4 @@ -const EXIT_DURATION = 0.5 +const SINGLE_DURATION = 0.1 export function setLines(parent, lines) { const oldLineElements = Array.from(parent.children) @@ -11,6 +11,10 @@ export function setLines(parent, lines) { const measureNew = measureProperties(newLineElements) + parent.style.setProperty( + "--duration", + `${SINGLE_DURATION}s` + ) fillAnimationStart(measureOld, measureNew) } @@ -39,6 +43,8 @@ function fillAnimationStart(measureOld, measureNew) { const dx = mOld.x - mNew.x const dy = mOld.y - mNew.y + const delay = staggerDelay(i, exitCounts, exitsDuration) + element.style.setProperty("--opacity", mOld.opacity) element.style.setProperty( "--transform", @@ -47,7 +53,7 @@ function fillAnimationStart(measureOld, measureNew) { element.style.setProperty("animation-name", "x") element.style.setProperty( "animation-delay", - `${staggerDelay(i, exitCounts, exitsDuration)}s` + `${delay}s` ) }) @@ -71,7 +77,7 @@ function fillAnimationStart(measureOld, measureNew) { }) const enterStart = - exitsDuration + (moves.length > 0 ? EXIT_DURATION : 0) + exitsDuration + (moves.length > 0 ? SINGLE_DURATION : 0) const enterCounts = enters.length const entersDuration = fullStaggerDuration(enterCounts) @@ -79,12 +85,7 @@ function fillAnimationStart(measureOld, measureNew) { const delay = enterStart + staggerDelay(i, enterCounts, entersDuration) - console.log({ - enterStart, - enterCounts, - entersDuration, - delay, - }) + console.log({ delay }) element.style.setProperty("--opacity", "0.1") element.style.setProperty( "--transform", @@ -199,12 +200,12 @@ function prepareToRemoveLine(oldElement) { function fullStaggerDuration(count) { if (count === 0) return 0 - return 2 * EXIT_DURATION * (1 - 1 / (1 + count)) + return 2 * SINGLE_DURATION * (1 - 1 / (1 + count)) } function staggerDelay(i, n, duration) { if (i === 0) return 0 - const max = duration - EXIT_DURATION + const max = duration - SINGLE_DURATION console.log({ i, n, max }) return (i / (n - 1)) * max } From 2952c1235317f3b788a2aee6342610420af369a7 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 1 Aug 2023 18:25:09 +0200 Subject: [PATCH 03/24] Init tokens demo --- packages/mdx/pages/tokens.js | 128 +++++++++++++++++++++++++++++++++++ packages/mdx/src/differ.js | 106 +++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 packages/mdx/pages/tokens.js create mode 100644 packages/mdx/src/differ.js diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js new file mode 100644 index 00000000..a836809f --- /dev/null +++ b/packages/mdx/pages/tokens.js @@ -0,0 +1,128 @@ +import React from "react" +import { diff, tokenize, withIds } from "../src/differ" + +export async function getStaticProps() { + const versions = await Promise.all( + code.map(async code => { + const result = await tokenize( + code, + "jsx", + "github-dark" + ) + return result + }) + ) + return { props: { versions } } +} + +export default function Home({ versions }) { + const [selected, setSelected] = React.useState( + versions[0] + ) + return ( + <> + + + + ) +} + +function Code({ tokens }) { + const tokensWithIds = withIds(tokens) + const prevTokens = usePrevProps(tokensWithIds) + return ( + + ) +} + +function CodeTransition({ currentTokens, previousTokens }) { + const ref = React.useRef() + let tokens = currentTokens + if (previousTokens) { + const result = diff(previousTokens, currentTokens) + tokens = result + } + console.log(tokens) + return ( +
+      {tokens.map(token =>
+        token.style ? (
+          
+            {token.content}
+          
+        ) : (
+          token.content
+        )
+      )}
+    
+ ) +} + +// prettier-ignore +const code = [` +import React from "react" + +const app = React.createElement( + "h1", + { style: { color: "teal" } }, + "Hello React" +) + +ReactDOM.render(app, document.getElementById("root")) +`.trim(),` +import React from "react" +import ReactDOM from "react-dom" + +const app =

Hello React

+ +ReactDOM.render(app, document.getElementById("root")) +`.trim(),` +import React from "react" +import ReactDOM from "react-dom" + +const app = ( +

+ Hello React +

+) + +ReactDOM.render(app, document.getElementById("root")) +`.trim(), +] + +function usePrevProps(props) { + const ref = React.useRef() + React.useEffect(() => { + ref.current = props + }) + return ref.current +} diff --git a/packages/mdx/src/differ.js b/packages/mdx/src/differ.js new file mode 100644 index 00000000..84371ae4 --- /dev/null +++ b/packages/mdx/src/differ.js @@ -0,0 +1,106 @@ +import { highlight } from "@code-hike/lighter" +import { diffArrays } from "diff" + +export async function tokenize(code, lang, theme) { + const { lines } = await highlight(code, lang, theme) + const tokens = lines.flatMap(line => [ + ...line, + { content: "\n" }, + ]) + // split trimming whitespace for each token + const splitTokens = tokens.flatMap(token => { + const [before, content, after] = + splitTrimmingWhitespace(token.content) + return [ + before?.length && { content: before }, + { content, style: token.style }, + after?.length && { content: after }, + ].filter(Boolean) + }) + + // join whitespace tokens + const joinedTokens = [] + splitTokens.forEach(token => { + const last = joinedTokens[joinedTokens.length - 1] + if (last && !last.style && !token.style) { + last.content += token.content + } else if (token.content === "") { + // ignore empty tokens + } else { + if (token.style == null) { + delete token.style + } + joinedTokens.push(token) + } + }) + + return joinedTokens +} + +// splitTrimmingWhitespace(" \t foo bar \n") should return [" \t ","foo bar"," \n"] +function splitTrimmingWhitespace(content) { + const trimmed = content.trim() + const before = content.slice(0, content.indexOf(trimmed)) + const after = content.slice( + content.indexOf(trimmed) + trimmed.length + ) + return [before, trimmed, after] +} + +export function withIds(tokens) { + let id = 0 + return tokens.map(token => + token.style ? { ...token, id: id++ } : token + ) +} + +export function diff(prev, next) { + if (!prev) { + return withIds(next) + } + + const ps = prev.filter(t => t.style) + const ns = next.filter(t => t.style) + + // console.log(Diff) + const result = diffArrays(ps, ns, { + comparator: (a, b) => a.content == b.content, + }) + + let nextIds = [] + let pIndex = 0 + + result.forEach(part => { + const { added, removed, count, value } = part + if (added) { + const before = ps[pIndex - 1]?.id + const after = ps[pIndex]?.id + nextIds = nextIds.concat(getIds(before, after, count)) + } else if (removed) { + pIndex += count + } else { + nextIds = nextIds.concat(value.map(t => t.id)) + pIndex += count + } + }) + + let nIndex = 0 + return next.map((token, i) => { + if (token.style) { + return { ...token, id: nextIds[nIndex++] } + } else { + return token + } + }) +} + +// n numbers between before and after (exclusive) +function getIds(before, after, n) { + let b = before == null ? after - 1 : before + let a = after == null ? before + 1 : after + const ids = [] + for (let i = 0; i < n; i++) { + ids.push(b + (a - b) * ((i + 1) / (n + 1))) + } + return ids +} From b1da1f2d70e0562afe7aadfd951dc01a1005a20a Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 1 Aug 2023 20:10:28 +0200 Subject: [PATCH 04/24] setTokens --- packages/mdx/pages/tokens.js | 132 ++++++++++++++++++++++++++++++----- packages/mdx/src/differ.js | 8 ++- 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js index a836809f..b2b3aa68 100644 --- a/packages/mdx/pages/tokens.js +++ b/packages/mdx/pages/tokens.js @@ -58,7 +58,11 @@ function CodeTransition({ currentTokens, previousTokens }) { const result = diff(previousTokens, currentTokens) tokens = result } - console.log(tokens) + + React.useLayoutEffect(() => { + setTokens(ref.current, previousTokens, tokens) + }, [currentTokens]) + return (
-      {tokens.map(token =>
-        token.style ? (
-          
-            {token.content}
-          
-        ) : (
-          token.content
-        )
-      )}
-    
+ /> ) } @@ -126,3 +117,112 @@ function usePrevProps(props) { }) return ref.current } + +function initTokens(parent, tokens) { + parent.innerHTML = "" + tokens.forEach((token, i) => { + parent.appendChild(createSpan(token)) + }) +} + +function setTokens(parent, prevTokens, nextTokens) { + if (!prevTokens) { + initTokens(parent, nextTokens) + return + } + + const prevSpanData = prevTokens.filter(t => t.style) + const nextSpanData = nextTokens.filter(t => t.style) + // console.log({ prevSpanData, nextSpanData }) + + const prevSpanRect = [] + const { x: parentX, y: parentY } = + parent.getBoundingClientRect() + + parent.childNodes.forEach(span => { + if (span.tagName !== "SPAN") return + const rect = span.getBoundingClientRect() + prevSpanRect.push({ + dx: rect.x - parentX, + dy: rect.y - parentY, + }) + }) + + initTokens(parent, nextTokens) + + const nextSpanRect = [] + parent.childNodes.forEach(span => { + if (span.tagName !== "SPAN") return + const rect = span.getBoundingClientRect() + + nextSpanRect.push({ + dx: rect.x - parentX, + dy: rect.y - parentY, + }) + }) + + // console.log({ prevSpanRect, nextSpanRect }) + + // change styles + parent.childNodes.forEach(span => { + if (span.tagName !== "SPAN") return + const id = Number(span.getAttribute("id")) + const prevIndex = prevSpanData.findIndex( + t => t.id === id + ) + const nextIndex = nextSpanData.findIndex( + t => t.id === id + ) + // console.log({ id, prevIndex, nextIndex }) + + if (prevIndex === -1) { + // console.log("+", nextSpanData[nextIndex].content) + span.style.setProperty("opacity", "0.1") + return + } + // console.log("=", nextSpanData[nextIndex].content) + + const dx = + prevSpanRect[prevIndex].dx - + nextSpanRect[nextIndex].dx + const dy = + prevSpanRect[prevIndex].dy - + nextSpanRect[nextIndex].dy + span.style.setProperty( + "transform", + `translateX(${dx}px) translateY(${dy}px)` + ) + }) + + const nextIds = nextSpanData.map(t => t.id) + // add removed tokens + prevSpanData.forEach((token, i) => { + if (!nextIds.includes(token.id)) { + const prevRect = prevSpanRect[i] + + const span = createSpan(token) + span.style.setProperty("top", `${prevRect.dy}px`) + span.style.setProperty("left", `${prevRect.dx}px`) + span.style.setProperty("position", "absolute") + parent.appendChild(span) + } + }) +} + +function createSpan(token) { + if (!token.style) { + return document.createTextNode(token.content) + } + const span = document.createElement("span") + span.textContent = token.content + + // set id + span.setAttribute("id", token.id) + + // set style + Object.entries(token.style).forEach(([key, value]) => { + span.style.setProperty(key, value) + }) + span.style.setProperty("display", "inline-block") + return span +} diff --git a/packages/mdx/src/differ.js b/packages/mdx/src/differ.js index 84371ae4..ec2c0ed9 100644 --- a/packages/mdx/src/differ.js +++ b/packages/mdx/src/differ.js @@ -79,19 +79,21 @@ export function diff(prev, next) { } else if (removed) { pIndex += count } else { - nextIds = nextIds.concat(value.map(t => t.id)) - pIndex += count + value.forEach(_ => { + nextIds.push(ps[pIndex++].id) + }) } }) let nIndex = 0 - return next.map((token, i) => { + const nextTokens = next.map(token => { if (token.style) { return { ...token, id: nextIds[nIndex++] } } else { return token } }) + return nextTokens } // n numbers between before and after (exclusive) From 01cb936fd01288e26088639057d5eb6b4e18e581 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 1 Aug 2023 20:17:24 +0200 Subject: [PATCH 05/24] Animate --- packages/mdx/pages/tokens.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js index b2b3aa68..c7f88f66 100644 --- a/packages/mdx/pages/tokens.js +++ b/packages/mdx/pages/tokens.js @@ -177,7 +177,11 @@ function setTokens(parent, prevTokens, nextTokens) { if (prevIndex === -1) { // console.log("+", nextSpanData[nextIndex].content) - span.style.setProperty("opacity", "0.1") + // span.style.setProperty("opacity", "0.1") + span.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: 1000, + fill: "forwards", + }) return } // console.log("=", nextSpanData[nextIndex].content) @@ -188,9 +192,21 @@ function setTokens(parent, prevTokens, nextTokens) { const dy = prevSpanRect[prevIndex].dy - nextSpanRect[nextIndex].dy - span.style.setProperty( - "transform", - `translateX(${dx}px) translateY(${dy}px)` + // span.style.setProperty( + // "transform", + // `translateX(${dx}px) translateY(${dy}px)` + // ) + span.animate( + [ + { + transform: `translateX(${dx}px) translateY(${dy}px)`, + }, + { transform: "none" }, + ], + { + duration: 1000, + fill: "forwards", + } ) }) @@ -205,6 +221,10 @@ function setTokens(parent, prevTokens, nextTokens) { span.style.setProperty("left", `${prevRect.dx}px`) span.style.setProperty("position", "absolute") parent.appendChild(span) + span.animate([{ opacity: 1 }, { opacity: 0.1 }], { + duration: 1000, + fill: "forwards", + }) } }) } From ed9cdfd580d393ca2e757449736e578861fb2d1c Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 1 Aug 2023 20:37:16 +0200 Subject: [PATCH 06/24] Add delays --- packages/mdx/pages/tokens.js | 10 ++++++---- packages/mdx/src/differ.js | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js index c7f88f66..1e170944 100644 --- a/packages/mdx/pages/tokens.js +++ b/packages/mdx/pages/tokens.js @@ -180,7 +180,8 @@ function setTokens(parent, prevTokens, nextTokens) { // span.style.setProperty("opacity", "0.1") span.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000, - fill: "forwards", + fill: "both", + delay: 2000, }) return } @@ -205,7 +206,8 @@ function setTokens(parent, prevTokens, nextTokens) { ], { duration: 1000, - fill: "forwards", + fill: "both", + delay: 1000, } ) }) @@ -221,9 +223,9 @@ function setTokens(parent, prevTokens, nextTokens) { span.style.setProperty("left", `${prevRect.dx}px`) span.style.setProperty("position", "absolute") parent.appendChild(span) - span.animate([{ opacity: 1 }, { opacity: 0.1 }], { + span.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 1000, - fill: "forwards", + fill: "both", }) } }) diff --git a/packages/mdx/src/differ.js b/packages/mdx/src/differ.js index ec2c0ed9..0964c832 100644 --- a/packages/mdx/src/differ.js +++ b/packages/mdx/src/differ.js @@ -2,7 +2,9 @@ import { highlight } from "@code-hike/lighter" import { diffArrays } from "diff" export async function tokenize(code, lang, theme) { - const { lines } = await highlight(code, lang, theme) + const { lines } = await highlight(code, lang, theme, { + scopes: true, + }) const tokens = lines.flatMap(line => [ ...line, { content: "\n" }, @@ -62,7 +64,6 @@ export function diff(prev, next) { const ps = prev.filter(t => t.style) const ns = next.filter(t => t.style) - // console.log(Diff) const result = diffArrays(ps, ns, { comparator: (a, b) => a.content == b.content, }) @@ -72,6 +73,7 @@ export function diff(prev, next) { result.forEach(part => { const { added, removed, count, value } = part + console.log(part) if (added) { const before = ps[pIndex - 1]?.id const after = ps[pIndex]?.id From 7c0b69603c8cd624a0e774810dadddcdb90cfc76 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Wed, 2 Aug 2023 14:22:42 +0200 Subject: [PATCH 07/24] Stagger --- packages/mdx/pages/tokens.js | 139 ++++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 35 deletions(-) diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js index 1e170944..225a328b 100644 --- a/packages/mdx/pages/tokens.js +++ b/packages/mdx/pages/tokens.js @@ -20,7 +20,9 @@ export default function Home({ versions }) { versions[0] ) return ( - <> +
- +
) } @@ -93,7 +95,6 @@ ReactDOM.render(app, document.getElementById("root")) import React from "react" import ReactDOM from "react-dom" -const app =

Hello React

ReactDOM.render(app, document.getElementById("root")) `.trim(),` @@ -125,6 +126,12 @@ function initTokens(parent, tokens) { }) } +const config = { + removeDuration: 100, + moveDuration: 300, + addDuration: 500, +} + function setTokens(parent, prevTokens, nextTokens) { if (!prevTokens) { initTokens(parent, nextTokens) @@ -163,6 +170,8 @@ function setTokens(parent, prevTokens, nextTokens) { // console.log({ prevSpanRect, nextSpanRect }) + const moved = [] + const added = [] // change styles parent.childNodes.forEach(span => { if (span.tagName !== "SPAN") return @@ -173,19 +182,11 @@ function setTokens(parent, prevTokens, nextTokens) { const nextIndex = nextSpanData.findIndex( t => t.id === id ) - // console.log({ id, prevIndex, nextIndex }) if (prevIndex === -1) { - // console.log("+", nextSpanData[nextIndex].content) - // span.style.setProperty("opacity", "0.1") - span.animate([{ opacity: 0 }, { opacity: 1 }], { - duration: 1000, - fill: "both", - delay: 2000, - }) + added.push({ span }) return } - // console.log("=", nextSpanData[nextIndex].content) const dx = prevSpanRect[prevIndex].dx - @@ -193,42 +194,97 @@ function setTokens(parent, prevTokens, nextTokens) { const dy = prevSpanRect[prevIndex].dy - nextSpanRect[nextIndex].dy - // span.style.setProperty( - // "transform", - // `translateX(${dx}px) translateY(${dy}px)` - // ) - span.animate( - [ - { - transform: `translateX(${dx}px) translateY(${dy}px)`, - }, - { transform: "none" }, - ], - { - duration: 1000, - fill: "both", - delay: 1000, - } - ) + moved.push({ + span, + dx, + dy, + fromColor: prevSpanData[prevIndex].style.color, + toColor: nextSpanData[nextIndex].style.color, + }) }) const nextIds = nextSpanData.map(t => t.id) - // add removed tokens + const removed = [] prevSpanData.forEach((token, i) => { if (!nextIds.includes(token.id)) { const prevRect = prevSpanRect[i] - const span = createSpan(token) span.style.setProperty("top", `${prevRect.dy}px`) span.style.setProperty("left", `${prevRect.dx}px`) span.style.setProperty("position", "absolute") parent.appendChild(span) - span.animate([{ opacity: 1 }, { opacity: 0 }], { - duration: 1000, - fill: "both", - }) + removed.push({ span }) } }) + + const removeDuration = fullStaggerDuration( + removed.length, + config.removeDuration + ) + const moveDuration = fullStaggerDuration( + moved.length, + config.moveDuration + ) + const addDuration = fullStaggerDuration( + added.length, + config.addDuration + ) + + removed.forEach(({ span }, i) => { + span.animate([{ opacity: 1 }, { opacity: 0 }], { + duration: removeDuration, + fill: "both", + easing: "ease-out", + delay: staggerDelay( + i, + removed.length, + removeDuration, + config.removeDuration + ), + }) + }) + + moved.forEach( + ({ span, dx, dy, fromColor, toColor }, i) => { + const transform = `translateX(${dx}px) translateY(${dy}px)` + span.animate( + [ + { transform, color: fromColor }, + { transform: "none", color: toColor }, + ], + { + duration: config.moveDuration, + fill: "both", + easing: "ease-in-out", + delay: + removeDuration + + staggerDelay( + i, + moved.length, + moveDuration, + config.moveDuration + ), + } + ) + } + ) + + added.forEach(({ span }, i) => { + span.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: config.addDuration, + fill: "both", + easing: "ease-in", + delay: + removeDuration + + config.moveDuration + + staggerDelay( + i, + added.length, + addDuration, + config.addDuration + ), + }) + }) } function createSpan(token) { @@ -248,3 +304,16 @@ function createSpan(token) { span.style.setProperty("display", "inline-block") return span } + +function fullStaggerDuration(count, singleDuration) { + if (count === 0) return 0 + // return 2 * singleDuration * (1 - 1 / (1 + count)) + return 1.5 * singleDuration - 1 / (1 + count) +} + +function staggerDelay(i, n, duration, singleDuration) { + if (i === 0) return 0 + const max = duration - singleDuration + console.log({ i, n, max }) + return (i / (n - 1)) * max +} From bce1689e7a3fb4e217ecc664feda6d4ee02139d0 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Wed, 2 Aug 2023 15:44:48 +0200 Subject: [PATCH 08/24] Inputs --- packages/mdx/pages/tokens.js | 450 ++++++++++++------------------ packages/mdx/src/differ.js | 43 ++- packages/mdx/src/smooth-tokens.js | 246 ++++++++++++++++ 3 files changed, 463 insertions(+), 276 deletions(-) create mode 100644 packages/mdx/src/smooth-tokens.js diff --git a/packages/mdx/pages/tokens.js b/packages/mdx/pages/tokens.js index 225a328b..068d7308 100644 --- a/packages/mdx/pages/tokens.js +++ b/packages/mdx/pages/tokens.js @@ -1,82 +1,189 @@ +import { Code } from "../src/smooth-tokens" +import { tokenize, tokenizeSync } from "../src/differ" +import { + THEME_NAMES, + LANG_NAMES, + getThemeColorsSync, + preload, +} from "@code-hike/lighter" import React from "react" -import { diff, tokenize, withIds } from "../src/differ" -export async function getStaticProps() { - const versions = await Promise.all( - code.map(async code => { - const result = await tokenize( - code, - "jsx", - "github-dark" - ) - return result - }) - ) - return { props: { versions } } -} +export default function Page() { + const [ready, setReady] = React.useState(false) -export default function Home({ versions }) { - const [selected, setSelected] = React.useState( - versions[0] - ) - return ( -
- - -
- ) -} + React.useEffect(() => { + preload(["jsx"], "github-dark").then(() => { + setReady(true) + }) + }, []) + + if (!ready) { + return ( +
+ ) + } -function Code({ tokens }) { - const tokensWithIds = withIds(tokens) - const prevTokens = usePrevProps(tokensWithIds) - return ( - - ) + return
} -function CodeTransition({ currentTokens, previousTokens }) { - const ref = React.useRef() - let tokens = currentTokens - if (previousTokens) { - const result = diff(previousTokens, currentTokens) - tokens = result - } - - React.useLayoutEffect(() => { - setTokens(ref.current, previousTokens, tokens) - }, [currentTokens]) +function Main() { + const [theme, setTheme] = React.useState("github-dark") + const [fromText, setFromText] = React.useState(code[0]) + const [toText, setToText] = React.useState(code[1]) + const [fromLang, setFromLang] = React.useState("jsx") + const [fromLangLoaded, setFromLangLoaded] = + React.useState("jsx") + const [toLang, setToLang] = React.useState("jsx") + const [toLangLoaded, setToLangLoaded] = + React.useState("jsx") + const [right, setRight] = React.useState(false) + + const themeColors = getThemeColorsSync(theme) + const tokens = React.useMemo(() => { + return right + ? tokenizeSync(toText, toLangLoaded, theme) + : tokenizeSync(fromText, fromLangLoaded, theme) + }, [ + right, + toText, + toLangLoaded, + fromText, + fromLangLoaded, + theme, + ]) return ( -
+    >
+      
+      
+      
+
+ { + const name = e.target.value + setFromLang(name) + if (LANG_NAMES.includes(name)) { + preload([name]).then(() => { + setFromLangLoaded(name) + }) + } + }} + style={{ + color: LANG_NAMES.includes(fromLang) + ? "black" + : "red", + }} + /> +