animot-presenter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +582 -0
- package/dist/AnimotPresenter.svelte +979 -0
- package/dist/AnimotPresenter.svelte.d.ts +14 -0
- package/dist/cdn/animot-presenter.css +1 -0
- package/dist/cdn/animot-presenter.esm.js +12427 -0
- package/dist/cdn/animot-presenter.min.js +16 -0
- package/dist/effects/ConfettiEffect.svelte +83 -0
- package/dist/effects/ConfettiEffect.svelte.d.ts +9 -0
- package/dist/effects/ParticlesBackground.svelte +114 -0
- package/dist/effects/ParticlesBackground.svelte.d.ts +9 -0
- package/dist/element.d.ts +24 -0
- package/dist/element.js +128 -0
- package/dist/engine/utils.d.ts +22 -0
- package/dist/engine/utils.js +72 -0
- package/dist/highlight/CodeMorph.svelte +136 -0
- package/dist/highlight/CodeMorph.svelte.d.ts +16 -0
- package/dist/highlight/highlighter-web.d.ts +10 -0
- package/dist/highlight/highlighter-web.js +54 -0
- package/dist/highlight/highlighter.d.ts +6 -0
- package/dist/highlight/highlighter.js +45 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/renderers/ChartRenderer.svelte +341 -0
- package/dist/renderers/ChartRenderer.svelte.d.ts +8 -0
- package/dist/renderers/CounterRenderer.svelte +64 -0
- package/dist/renderers/CounterRenderer.svelte.d.ts +8 -0
- package/dist/renderers/IconRenderer.svelte +18 -0
- package/dist/renderers/IconRenderer.svelte.d.ts +7 -0
- package/dist/styles/presenter.css +48 -0
- package/dist/types.d.ts +319 -0
- package/dist/types.js +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import confetti from 'canvas-confetti';
|
|
4
|
+
import type { ConfettiConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
interface Props { config: ConfettiConfig; width: number; height: number; }
|
|
7
|
+
let { config, width, height }: Props = $props();
|
|
8
|
+
|
|
9
|
+
let canvasEl: HTMLCanvasElement;
|
|
10
|
+
let myConfetti: confetti.CreateTypes | null = null;
|
|
11
|
+
let intervalId: ReturnType<typeof setInterval>;
|
|
12
|
+
|
|
13
|
+
function fireBurst() {
|
|
14
|
+
if (!myConfetti) return;
|
|
15
|
+
myConfetti({
|
|
16
|
+
particleCount: config.particleCount, spread: config.spread,
|
|
17
|
+
startVelocity: config.startVelocity, gravity: config.gravity,
|
|
18
|
+
drift: config.drift, scalar: config.scalar, colors: config.colors,
|
|
19
|
+
ticks: 200, origin: { x: 0.5, y: 0.5 }, disableForReducedMotion: false
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fireContinuous() {
|
|
24
|
+
if (!myConfetti) return;
|
|
25
|
+
myConfetti({
|
|
26
|
+
particleCount: Math.max(1, Math.floor(config.particleCount / 10)),
|
|
27
|
+
spread: config.spread, startVelocity: config.startVelocity * 0.6,
|
|
28
|
+
gravity: config.gravity, drift: config.drift, scalar: config.scalar,
|
|
29
|
+
colors: config.colors, ticks: 300, origin: { x: Math.random(), y: -0.05 },
|
|
30
|
+
disableForReducedMotion: false
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fireFireworks() {
|
|
35
|
+
if (!myConfetti) return;
|
|
36
|
+
myConfetti({
|
|
37
|
+
particleCount: config.particleCount, spread: 360,
|
|
38
|
+
startVelocity: config.startVelocity, gravity: config.gravity * 0.8,
|
|
39
|
+
drift: 0, scalar: config.scalar, colors: config.colors,
|
|
40
|
+
ticks: 150, origin: { x: 0.2 + Math.random() * 0.6, y: 0.2 + Math.random() * 0.4 },
|
|
41
|
+
disableForReducedMotion: false
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fireSnow() {
|
|
46
|
+
if (!myConfetti) return;
|
|
47
|
+
myConfetti({
|
|
48
|
+
particleCount: Math.max(1, Math.floor(config.particleCount / 8)),
|
|
49
|
+
spread: 180, startVelocity: 2, gravity: config.gravity * 0.3,
|
|
50
|
+
drift: config.drift, scalar: config.scalar * 0.8,
|
|
51
|
+
colors: ['#ffffff', '#e0e0ff', '#d0d8ff'], ticks: 600,
|
|
52
|
+
origin: { x: Math.random(), y: -0.05 }, shapes: ['circle'],
|
|
53
|
+
disableForReducedMotion: false
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function startEffect() {
|
|
58
|
+
stopEffect();
|
|
59
|
+
if (!myConfetti || !config.enabled) return;
|
|
60
|
+
switch (config.mode) {
|
|
61
|
+
case 'burst': fireBurst(); break;
|
|
62
|
+
case 'continuous': fireContinuous(); intervalId = setInterval(fireContinuous, 250); break;
|
|
63
|
+
case 'fireworks': fireFireworks(); intervalId = setInterval(fireFireworks, 800); break;
|
|
64
|
+
case 'snow': fireSnow(); intervalId = setInterval(fireSnow, 150); break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stopEffect() { if (intervalId) clearInterval(intervalId); }
|
|
69
|
+
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const _ = [config.enabled, config.mode, config.particleCount, config.spread, config.colors, config.gravity, config.drift, config.startVelocity, config.scalar];
|
|
72
|
+
if (myConfetti && config.enabled) startEffect();
|
|
73
|
+
else stopEffect();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
onMount(() => {
|
|
77
|
+
myConfetti = confetti.create(canvasEl, { resize: false, useWorker: false });
|
|
78
|
+
if (config.enabled) startEffect();
|
|
79
|
+
return () => { stopEffect(); if (myConfetti) myConfetti.reset(); };
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<canvas bind:this={canvasEl} {width} {height} style="position:absolute;inset:0;pointer-events:none;z-index:1;"></canvas>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ConfettiConfig } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
config: ConfettiConfig;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
declare const ConfettiEffect: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type ConfettiEffect = ReturnType<typeof ConfettiEffect>;
|
|
9
|
+
export default ConfettiEffect;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { ParticlesConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
interface Props { config: ParticlesConfig; width: number; height: number; }
|
|
6
|
+
let { config, width, height }: Props = $props();
|
|
7
|
+
|
|
8
|
+
let canvasEl: HTMLCanvasElement;
|
|
9
|
+
let animFrameId: number;
|
|
10
|
+
|
|
11
|
+
interface Particle { x: number; y: number; vx: number; vy: number; size: number; opacity: number; }
|
|
12
|
+
let particles: Particle[] = [];
|
|
13
|
+
|
|
14
|
+
function initParticles() {
|
|
15
|
+
particles = [];
|
|
16
|
+
for (let i = 0; i < config.count; i++) particles.push(createParticle(true));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createParticle(randomPosition: boolean): Particle {
|
|
20
|
+
const size = config.minSize + Math.random() * (config.maxSize - config.minSize);
|
|
21
|
+
const speed = config.speed * 0.5;
|
|
22
|
+
let vx = 0, vy = 0;
|
|
23
|
+
switch (config.direction) {
|
|
24
|
+
case 'up': vx = (Math.random() - 0.5) * speed * 0.3; vy = -speed * (0.3 + Math.random() * 0.7); break;
|
|
25
|
+
case 'down': vx = (Math.random() - 0.5) * speed * 0.3; vy = speed * (0.3 + Math.random() * 0.7); break;
|
|
26
|
+
case 'left': vx = -speed * (0.3 + Math.random() * 0.7); vy = (Math.random() - 0.5) * speed * 0.3; break;
|
|
27
|
+
case 'right': vx = speed * (0.3 + Math.random() * 0.7); vy = (Math.random() - 0.5) * speed * 0.3; break;
|
|
28
|
+
default: { const angle = Math.random() * Math.PI * 2; vx = Math.cos(angle) * speed * (0.3 + Math.random() * 0.7); vy = Math.sin(angle) * speed * (0.3 + Math.random() * 0.7); }
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
x: randomPosition ? Math.random() * width : (config.direction === 'right' ? -size : config.direction === 'left' ? width + size : Math.random() * width),
|
|
32
|
+
y: randomPosition ? Math.random() * height : (config.direction === 'down' ? -size : config.direction === 'up' ? height + size : Math.random() * height),
|
|
33
|
+
vx, vy, size, opacity: config.opacity * (0.3 + Math.random() * 0.7),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function drawShape(ctx: CanvasRenderingContext2D, p: Particle) {
|
|
38
|
+
ctx.globalAlpha = p.opacity;
|
|
39
|
+
ctx.fillStyle = config.color;
|
|
40
|
+
switch (config.shape) {
|
|
41
|
+
case 'square': ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size); break;
|
|
42
|
+
case 'triangle': {
|
|
43
|
+
ctx.beginPath(); ctx.moveTo(p.x, p.y - p.size);
|
|
44
|
+
ctx.lineTo(p.x - p.size * 0.866, p.y + p.size * 0.5);
|
|
45
|
+
ctx.lineTo(p.x + p.size * 0.866, p.y + p.size * 0.5);
|
|
46
|
+
ctx.closePath(); ctx.fill(); break;
|
|
47
|
+
}
|
|
48
|
+
case 'star': {
|
|
49
|
+
ctx.beginPath();
|
|
50
|
+
for (let i = 0; i < 5; i++) {
|
|
51
|
+
const outerAngle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
|
52
|
+
const innerAngle = outerAngle + Math.PI / 5;
|
|
53
|
+
const outerR = p.size, innerR = p.size * 0.4;
|
|
54
|
+
if (i === 0) ctx.moveTo(p.x + outerR * Math.cos(outerAngle), p.y + outerR * Math.sin(outerAngle));
|
|
55
|
+
else ctx.lineTo(p.x + outerR * Math.cos(outerAngle), p.y + outerR * Math.sin(outerAngle));
|
|
56
|
+
ctx.lineTo(p.x + innerR * Math.cos(innerAngle), p.y + innerR * Math.sin(innerAngle));
|
|
57
|
+
}
|
|
58
|
+
ctx.closePath(); ctx.fill(); break;
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function drawConnections(ctx: CanvasRenderingContext2D) {
|
|
66
|
+
if (config.connectDistance <= 0) return;
|
|
67
|
+
const maxDist = config.connectDistance;
|
|
68
|
+
const maxDistSq = maxDist * maxDist;
|
|
69
|
+
ctx.strokeStyle = config.color; ctx.lineWidth = 1;
|
|
70
|
+
for (let i = 0; i < particles.length; i++) {
|
|
71
|
+
for (let j = i + 1; j < particles.length; j++) {
|
|
72
|
+
const dx = particles[i].x - particles[j].x;
|
|
73
|
+
const dy = particles[i].y - particles[j].y;
|
|
74
|
+
const distSq = dx * dx + dy * dy;
|
|
75
|
+
if (distSq < maxDistSq) {
|
|
76
|
+
ctx.globalAlpha = config.opacity * 0.6 * (1 - Math.sqrt(distSq) / maxDist);
|
|
77
|
+
ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y);
|
|
78
|
+
ctx.lineTo(particles[j].x, particles[j].y); ctx.stroke();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function animate() {
|
|
85
|
+
const ctx = canvasEl?.getContext('2d');
|
|
86
|
+
if (!ctx) return;
|
|
87
|
+
ctx.clearRect(0, 0, width, height);
|
|
88
|
+
drawConnections(ctx);
|
|
89
|
+
const margin = config.maxSize * 2;
|
|
90
|
+
for (const p of particles) {
|
|
91
|
+
p.x += p.vx; p.y += p.vy;
|
|
92
|
+
if (p.x < -margin) p.x = width + margin;
|
|
93
|
+
if (p.x > width + margin) p.x = -margin;
|
|
94
|
+
if (p.y < -margin) p.y = height + margin;
|
|
95
|
+
if (p.y > height + margin) p.y = -margin;
|
|
96
|
+
drawShape(ctx, p);
|
|
97
|
+
}
|
|
98
|
+
ctx.globalAlpha = 1;
|
|
99
|
+
animFrameId = requestAnimationFrame(animate);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$effect(() => {
|
|
103
|
+
const _ = [config.count, config.color, config.opacity, config.minSize, config.maxSize, config.speed, config.shape, config.connectDistance, config.direction, width, height];
|
|
104
|
+
if (canvasEl) initParticles();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
onMount(() => {
|
|
108
|
+
initParticles();
|
|
109
|
+
animFrameId = requestAnimationFrame(animate);
|
|
110
|
+
return () => { if (animFrameId) cancelAnimationFrame(animFrameId); };
|
|
111
|
+
});
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<canvas bind:this={canvasEl} {width} {height} style="position:absolute;inset:0;pointer-events:none;z-index:0;"></canvas>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ParticlesConfig } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
config: ParticlesConfig;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
declare const ParticlesBackground: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type ParticlesBackground = ReturnType<typeof ParticlesBackground>;
|
|
9
|
+
export default ParticlesBackground;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnimotProject } from './types';
|
|
2
|
+
import './styles/presenter.css';
|
|
3
|
+
declare class AnimotPresenterElement extends HTMLElement {
|
|
4
|
+
static get observedAttributes(): string[];
|
|
5
|
+
private _component;
|
|
6
|
+
private _data;
|
|
7
|
+
private _onslidechange;
|
|
8
|
+
private _oncomplete;
|
|
9
|
+
set data(value: AnimotProject | null);
|
|
10
|
+
get data(): AnimotProject | null;
|
|
11
|
+
set onslidechange(fn: ((index: number, total: number) => void) | null);
|
|
12
|
+
set oncomplete(fn: (() => void) | null);
|
|
13
|
+
connectedCallback(): void;
|
|
14
|
+
disconnectedCallback(): void;
|
|
15
|
+
attributeChangedCallback(): void;
|
|
16
|
+
private _getProps;
|
|
17
|
+
private _mount;
|
|
18
|
+
private _unmount;
|
|
19
|
+
private _remount;
|
|
20
|
+
goto(index: number): void;
|
|
21
|
+
next(): void;
|
|
22
|
+
prev(): void;
|
|
23
|
+
}
|
|
24
|
+
export { AnimotPresenterElement };
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Component wrapper for AnimotPresenter.
|
|
3
|
+
* Registers <animot-presenter> custom element for use in any framework or vanilla HTML.
|
|
4
|
+
*/
|
|
5
|
+
import { mount, unmount } from 'svelte';
|
|
6
|
+
import AnimotPresenter from './AnimotPresenter.svelte';
|
|
7
|
+
// Inject floating animation CSS into the document (once)
|
|
8
|
+
import './styles/presenter.css';
|
|
9
|
+
const BOOLEAN_ATTRS = new Set(['autoplay', 'loop', 'controls', 'arrows', 'progress', 'keyboard']);
|
|
10
|
+
const NUMBER_ATTRS = new Set(['duration', 'start-slide']);
|
|
11
|
+
class AnimotPresenterElement extends HTMLElement {
|
|
12
|
+
static get observedAttributes() {
|
|
13
|
+
return ['src', 'autoplay', 'loop', 'controls', 'arrows', 'progress', 'keyboard', 'duration', 'start-slide'];
|
|
14
|
+
}
|
|
15
|
+
_component = null;
|
|
16
|
+
_data = null;
|
|
17
|
+
_onslidechange = null;
|
|
18
|
+
_oncomplete = null;
|
|
19
|
+
// Public API: set data programmatically
|
|
20
|
+
set data(value) {
|
|
21
|
+
this._data = value;
|
|
22
|
+
this._remount();
|
|
23
|
+
}
|
|
24
|
+
get data() {
|
|
25
|
+
return this._data;
|
|
26
|
+
}
|
|
27
|
+
set onslidechange(fn) {
|
|
28
|
+
this._onslidechange = fn;
|
|
29
|
+
this._remount();
|
|
30
|
+
}
|
|
31
|
+
set oncomplete(fn) {
|
|
32
|
+
this._oncomplete = fn;
|
|
33
|
+
this._remount();
|
|
34
|
+
}
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
this._mount();
|
|
37
|
+
}
|
|
38
|
+
disconnectedCallback() {
|
|
39
|
+
this._unmount();
|
|
40
|
+
}
|
|
41
|
+
attributeChangedCallback() {
|
|
42
|
+
this._remount();
|
|
43
|
+
}
|
|
44
|
+
_getProps() {
|
|
45
|
+
const props = {};
|
|
46
|
+
// String attributes
|
|
47
|
+
const src = this.getAttribute('src');
|
|
48
|
+
if (src)
|
|
49
|
+
props.src = src;
|
|
50
|
+
// Boolean attributes (presence = true)
|
|
51
|
+
for (const attr of BOOLEAN_ATTRS) {
|
|
52
|
+
props[attr] = this.hasAttribute(attr);
|
|
53
|
+
}
|
|
54
|
+
// Number attributes
|
|
55
|
+
const duration = this.getAttribute('duration');
|
|
56
|
+
if (duration)
|
|
57
|
+
props.duration = Number(duration);
|
|
58
|
+
const startSlide = this.getAttribute('start-slide');
|
|
59
|
+
if (startSlide)
|
|
60
|
+
props.startSlide = Number(startSlide);
|
|
61
|
+
// Programmatic props
|
|
62
|
+
if (this._data)
|
|
63
|
+
props.data = this._data;
|
|
64
|
+
if (this._onslidechange) {
|
|
65
|
+
props.onslidechange = this._onslidechange;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Dispatch custom event for vanilla JS listeners
|
|
69
|
+
props.onslidechange = (index, total) => {
|
|
70
|
+
this.dispatchEvent(new CustomEvent('slidechange', { detail: { index, total }, bubbles: true }));
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (this._oncomplete) {
|
|
74
|
+
props.oncomplete = this._oncomplete;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
props.oncomplete = () => {
|
|
78
|
+
this.dispatchEvent(new CustomEvent('complete', { bubbles: true }));
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return props;
|
|
82
|
+
}
|
|
83
|
+
_mount() {
|
|
84
|
+
if (this._component)
|
|
85
|
+
return;
|
|
86
|
+
// Ensure the element has dimensions
|
|
87
|
+
if (!this.style.display) {
|
|
88
|
+
this.style.display = 'block';
|
|
89
|
+
}
|
|
90
|
+
this._component = mount(AnimotPresenter, {
|
|
91
|
+
target: this,
|
|
92
|
+
props: this._getProps()
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
_unmount() {
|
|
96
|
+
if (this._component) {
|
|
97
|
+
unmount(this._component);
|
|
98
|
+
this._component = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
_remount() {
|
|
102
|
+
if (!this.isConnected)
|
|
103
|
+
return;
|
|
104
|
+
this._unmount();
|
|
105
|
+
this._mount();
|
|
106
|
+
}
|
|
107
|
+
// Public methods that proxy to the Svelte component
|
|
108
|
+
goto(index) {
|
|
109
|
+
const btn = this.querySelector('.animot-controls button');
|
|
110
|
+
// Fire a custom event the component can listen to
|
|
111
|
+
this.dispatchEvent(new CustomEvent('animot-goto', { detail: { index } }));
|
|
112
|
+
}
|
|
113
|
+
next() {
|
|
114
|
+
const nextBtn = this.querySelectorAll('.animot-controls button')[2];
|
|
115
|
+
if (nextBtn && !nextBtn.disabled)
|
|
116
|
+
nextBtn.click();
|
|
117
|
+
}
|
|
118
|
+
prev() {
|
|
119
|
+
const prevBtn = this.querySelectorAll('.animot-controls button')[0];
|
|
120
|
+
if (prevBtn && !prevBtn.disabled)
|
|
121
|
+
prevBtn.click();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Register the custom element
|
|
125
|
+
if (typeof customElements !== 'undefined' && !customElements.get('animot-presenter')) {
|
|
126
|
+
customElements.define('animot-presenter', AnimotPresenterElement);
|
|
127
|
+
}
|
|
128
|
+
export { AnimotPresenterElement };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function easeInOutCubic(t: number): number;
|
|
2
|
+
export declare function interpolateColor(color1: string, color2: string, progress: number): string;
|
|
3
|
+
export declare function hashFraction(str: string, salt?: number): number;
|
|
4
|
+
export declare function getFloatAnimName(direction: string, seed: string): string;
|
|
5
|
+
export declare function computeFloatAmp(cfg: {
|
|
6
|
+
amplitude: number;
|
|
7
|
+
amplitudeRandomness?: number;
|
|
8
|
+
}, seed: string): number;
|
|
9
|
+
export declare function computeFloatSpeed(cfg: {
|
|
10
|
+
speed: number;
|
|
11
|
+
speedRandomness?: number;
|
|
12
|
+
}, seed: string): number;
|
|
13
|
+
export declare function getBackgroundStyle(bg: {
|
|
14
|
+
type: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
gradient?: {
|
|
17
|
+
type: string;
|
|
18
|
+
angle?: number;
|
|
19
|
+
colors: string[];
|
|
20
|
+
};
|
|
21
|
+
image?: string;
|
|
22
|
+
}): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function easeInOutCubic(t) {
|
|
2
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
3
|
+
}
|
|
4
|
+
export function interpolateColor(color1, color2, progress) {
|
|
5
|
+
if (color1 === 'transparent')
|
|
6
|
+
color1 = '#00000000';
|
|
7
|
+
if (color2 === 'transparent')
|
|
8
|
+
color2 = '#00000000';
|
|
9
|
+
const hex1 = color1.replace('#', '');
|
|
10
|
+
const hex2 = color2.replace('#', '');
|
|
11
|
+
const r1 = parseInt(hex1.substring(0, 2), 16) || 0;
|
|
12
|
+
const g1 = parseInt(hex1.substring(2, 4), 16) || 0;
|
|
13
|
+
const b1 = parseInt(hex1.substring(4, 6), 16) || 0;
|
|
14
|
+
const a1 = hex1.length === 8 ? parseInt(hex1.substring(6, 8), 16) / 255 : 1;
|
|
15
|
+
const r2 = parseInt(hex2.substring(0, 2), 16) || 0;
|
|
16
|
+
const g2 = parseInt(hex2.substring(2, 4), 16) || 0;
|
|
17
|
+
const b2 = parseInt(hex2.substring(4, 6), 16) || 0;
|
|
18
|
+
const a2 = hex2.length === 8 ? parseInt(hex2.substring(6, 8), 16) / 255 : 1;
|
|
19
|
+
const r = Math.round(r1 + (r2 - r1) * progress);
|
|
20
|
+
const g = Math.round(g1 + (g2 - g1) * progress);
|
|
21
|
+
const b = Math.round(b1 + (b2 - b1) * progress);
|
|
22
|
+
const a = a1 + (a2 - a1) * progress;
|
|
23
|
+
if (a < 1) {
|
|
24
|
+
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
25
|
+
}
|
|
26
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
export function hashFraction(str, salt = 0) {
|
|
29
|
+
let hash = salt;
|
|
30
|
+
for (let i = 0; i < str.length; i++) {
|
|
31
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
32
|
+
hash |= 0;
|
|
33
|
+
}
|
|
34
|
+
return (Math.abs(hash) % 1000) / 1000;
|
|
35
|
+
}
|
|
36
|
+
export function getFloatAnimName(direction, seed) {
|
|
37
|
+
const h = Math.floor(hashFraction(seed) * 100);
|
|
38
|
+
switch (direction) {
|
|
39
|
+
case 'vertical': return 'float-vertical';
|
|
40
|
+
case 'horizontal': return 'float-horizontal';
|
|
41
|
+
case 'both':
|
|
42
|
+
default:
|
|
43
|
+
return `float-both-${h % 4}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function computeFloatAmp(cfg, seed) {
|
|
47
|
+
const r = cfg.amplitudeRandomness ?? 0;
|
|
48
|
+
if (r === 0)
|
|
49
|
+
return cfg.amplitude;
|
|
50
|
+
return cfg.amplitude * (1 - r + r * hashFraction(seed, 1));
|
|
51
|
+
}
|
|
52
|
+
export function computeFloatSpeed(cfg, seed) {
|
|
53
|
+
const r = cfg.speedRandomness ?? 0;
|
|
54
|
+
if (r === 0)
|
|
55
|
+
return cfg.speed;
|
|
56
|
+
return cfg.speed * (1 - r + r * hashFraction(seed, 2));
|
|
57
|
+
}
|
|
58
|
+
export function getBackgroundStyle(bg) {
|
|
59
|
+
if (bg.type === 'transparent')
|
|
60
|
+
return 'background: transparent';
|
|
61
|
+
if (bg.type === 'solid')
|
|
62
|
+
return `background-color: ${bg.color ?? 'transparent'}`;
|
|
63
|
+
if (bg.type === 'gradient' && bg.gradient) {
|
|
64
|
+
const { type, angle = 135, colors } = bg.gradient;
|
|
65
|
+
if (type === 'linear')
|
|
66
|
+
return `background: linear-gradient(${angle}deg, ${colors.join(', ')})`;
|
|
67
|
+
return `background: radial-gradient(circle, ${colors.join(', ')})`;
|
|
68
|
+
}
|
|
69
|
+
if (bg.type === 'image' && bg.image)
|
|
70
|
+
return `background-image: url(${bg.image}); background-size: cover; background-position: center`;
|
|
71
|
+
return 'background: transparent';
|
|
72
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { highlightCode } from './highlighter';
|
|
4
|
+
import type { CodeAnimationMode } from '../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
oldCode: string;
|
|
8
|
+
newCode: string;
|
|
9
|
+
language: string;
|
|
10
|
+
theme: string;
|
|
11
|
+
mode: CodeAnimationMode;
|
|
12
|
+
speed?: number;
|
|
13
|
+
highlightColor?: string;
|
|
14
|
+
highlightDuration?: number;
|
|
15
|
+
showLineNumbers?: boolean;
|
|
16
|
+
onComplete?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
oldCode, newCode, language = 'javascript', theme = 'github-dark',
|
|
21
|
+
mode = 'highlight-changes', speed = 50, highlightColor = '#facc15',
|
|
22
|
+
highlightDuration = 1000, showLineNumbers = false, onComplete
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const durationSec = $derived(highlightDuration / 1000);
|
|
26
|
+
let displayedHtml = $state('');
|
|
27
|
+
let changedWordIndices = $state<Set<number>>(new Set());
|
|
28
|
+
let isAnimating = $state(false);
|
|
29
|
+
|
|
30
|
+
function tokenize(code: string): string[] {
|
|
31
|
+
return code.match(/[a-zA-Z_][a-zA-Z0-9_]*|\d+|\s+|./g) || [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findChangedWords(oldStr: string, newStr: string): Set<number> {
|
|
35
|
+
const oldTokens = tokenize(oldStr);
|
|
36
|
+
const newTokens = tokenize(newStr);
|
|
37
|
+
const changed = new Set<number>();
|
|
38
|
+
let oldIdx = 0, newIdx = 0;
|
|
39
|
+
while (newIdx < newTokens.length) {
|
|
40
|
+
if (oldIdx < oldTokens.length && oldTokens[oldIdx] === newTokens[newIdx]) {
|
|
41
|
+
oldIdx++; newIdx++;
|
|
42
|
+
} else {
|
|
43
|
+
if (newTokens[newIdx].trim()) changed.add(newIdx);
|
|
44
|
+
newIdx++;
|
|
45
|
+
if (oldIdx < oldTokens.length && oldTokens[oldIdx] !== newTokens[newIdx - 1]) oldIdx++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return changed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sleep(ms: number): Promise<void> { return new Promise(r => setTimeout(r, ms)); }
|
|
52
|
+
|
|
53
|
+
async function animateTypewriter() {
|
|
54
|
+
isAnimating = true;
|
|
55
|
+
let diffStart = 0;
|
|
56
|
+
while (diffStart < oldCode.length && diffStart < newCode.length && oldCode[diffStart] === newCode[diffStart]) diffStart++;
|
|
57
|
+
const delayMs = 1000 / speed;
|
|
58
|
+
for (let i = diffStart; i <= newCode.length; i++) {
|
|
59
|
+
displayedHtml = await highlightCode(newCode.substring(0, i), language, theme, { showLineNumbers });
|
|
60
|
+
await sleep(delayMs);
|
|
61
|
+
}
|
|
62
|
+
isAnimating = false;
|
|
63
|
+
onComplete?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function animateHighlight() {
|
|
67
|
+
isAnimating = true;
|
|
68
|
+
changedWordIndices = findChangedWords(oldCode, newCode);
|
|
69
|
+
const baseHtml = await highlightCode(newCode, language, theme, { showLineNumbers });
|
|
70
|
+
displayedHtml = wrapChangedWords(newCode, baseHtml, changedWordIndices);
|
|
71
|
+
await sleep(highlightDuration);
|
|
72
|
+
displayedHtml = baseHtml;
|
|
73
|
+
changedWordIndices = new Set();
|
|
74
|
+
isAnimating = false;
|
|
75
|
+
onComplete?.();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function wrapChangedWords(code: string, html: string, changed: Set<number>): string {
|
|
79
|
+
if (changed.size === 0) return html;
|
|
80
|
+
const tokens = tokenize(code);
|
|
81
|
+
const changedTokens = new Set<string>();
|
|
82
|
+
for (const idx of changed) {
|
|
83
|
+
if (tokens[idx] && tokens[idx].trim()) changedTokens.add(tokens[idx]);
|
|
84
|
+
}
|
|
85
|
+
let result = html;
|
|
86
|
+
for (const token of changedTokens) {
|
|
87
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
88
|
+
const regex = new RegExp(`(>)([^<]*?)(\\b${escaped}\\b)([^<]*?)(<)`, 'g');
|
|
89
|
+
result = result.replace(regex, (_, open, before, word, after, close) => {
|
|
90
|
+
return `${open}${before}<mark class="word-highlight">${word}</mark>${after}${close}`;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onMount(async () => {
|
|
97
|
+
if (mode === 'instant' || oldCode === newCode) {
|
|
98
|
+
displayedHtml = await highlightCode(newCode, language, theme, { showLineNumbers });
|
|
99
|
+
} else if (mode === 'typewriter') {
|
|
100
|
+
await animateTypewriter();
|
|
101
|
+
} else if (mode === 'highlight-changes') {
|
|
102
|
+
await animateHighlight();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<div class="code-morph" class:animating={isAnimating} style:--highlight-color={highlightColor} style:--duration="{durationSec}s">{@html displayedHtml}</div>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.code-morph {
|
|
111
|
+
width: 100%; height: 100%; margin: 0; padding: 0;
|
|
112
|
+
background: transparent; font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
113
|
+
font-size: inherit; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;
|
|
114
|
+
color: #e6edf3; box-sizing: border-box; overflow: visible;
|
|
115
|
+
}
|
|
116
|
+
.code-morph :global(pre) {
|
|
117
|
+
margin: 0; padding: 16px; background: transparent !important;
|
|
118
|
+
font-family: var(--font-mono); font-size: inherit; line-height: 1.6; overflow: visible;
|
|
119
|
+
}
|
|
120
|
+
.code-morph :global(code) { font-family: inherit; font-size: inherit; font-weight: inherit; }
|
|
121
|
+
.code-morph :global(.line-number) {
|
|
122
|
+
display: inline-block; width: 2.5em; margin-right: 1em;
|
|
123
|
+
text-align: right; color: #6e7681; user-select: none; opacity: 0.6;
|
|
124
|
+
}
|
|
125
|
+
.code-morph :global(.word-highlight) {
|
|
126
|
+
background: var(--highlight-color, #facc15); color: #000;
|
|
127
|
+
border-radius: 3px; padding: 1px 3px; margin: 0 -3px;
|
|
128
|
+
box-decoration-break: clone;
|
|
129
|
+
animation: fadeHighlight var(--duration, 1s) ease-out forwards;
|
|
130
|
+
}
|
|
131
|
+
@keyframes fadeHighlight {
|
|
132
|
+
0% { background: var(--highlight-color, #facc15); color: #000; }
|
|
133
|
+
70% { background: var(--highlight-color, #facc15); color: #000; }
|
|
134
|
+
100% { background: transparent; color: inherit; }
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CodeAnimationMode } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
oldCode: string;
|
|
4
|
+
newCode: string;
|
|
5
|
+
language: string;
|
|
6
|
+
theme: string;
|
|
7
|
+
mode: CodeAnimationMode;
|
|
8
|
+
speed?: number;
|
|
9
|
+
highlightColor?: string;
|
|
10
|
+
highlightDuration?: number;
|
|
11
|
+
showLineNumbers?: boolean;
|
|
12
|
+
onComplete?: () => void;
|
|
13
|
+
}
|
|
14
|
+
declare const CodeMorph: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type CodeMorph = ReturnType<typeof CodeMorph>;
|
|
16
|
+
export default CodeMorph;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN-only highlighter that uses shiki/bundle/web (fewer languages, smaller bundle).
|
|
3
|
+
* The Svelte package uses highlighter.ts which imports from full shiki.
|
|
4
|
+
*/
|
|
5
|
+
import { type Highlighter } from 'shiki/bundle/web';
|
|
6
|
+
export declare function getHighlighter(): Promise<Highlighter>;
|
|
7
|
+
export interface HighlightOptions {
|
|
8
|
+
showLineNumbers?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function highlightCode(code: string, language: string, theme?: string, options?: HighlightOptions): Promise<string>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDN-only highlighter that uses shiki/bundle/web (fewer languages, smaller bundle).
|
|
3
|
+
* The Svelte package uses highlighter.ts which imports from full shiki.
|
|
4
|
+
*/
|
|
5
|
+
import { createHighlighter } from 'shiki/bundle/web';
|
|
6
|
+
let highlighterPromise = null;
|
|
7
|
+
const THEMES = ['github-dark', 'github-light', 'dracula', 'nord', 'one-dark-pro', 'vitesse-dark'];
|
|
8
|
+
export async function getHighlighter() {
|
|
9
|
+
if (!highlighterPromise) {
|
|
10
|
+
highlighterPromise = createHighlighter({ themes: THEMES, langs: [] });
|
|
11
|
+
}
|
|
12
|
+
return highlighterPromise;
|
|
13
|
+
}
|
|
14
|
+
export async function highlightCode(code, language, theme = 'github-dark', options = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const highlighter = await getHighlighter();
|
|
17
|
+
const loadedLangs = highlighter.getLoadedLanguages();
|
|
18
|
+
if (!loadedLangs.includes(language)) {
|
|
19
|
+
// Lazy-load the requested language from the web bundle
|
|
20
|
+
try {
|
|
21
|
+
await highlighter.loadLanguage(language);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Language not available in web bundle, fall back to plaintext
|
|
25
|
+
language = 'javascript';
|
|
26
|
+
if (!loadedLangs.includes('javascript')) {
|
|
27
|
+
await highlighter.loadLanguage('javascript');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const html = highlighter.codeToHtml(code, { lang: language, theme });
|
|
32
|
+
if (options.showLineNumbers)
|
|
33
|
+
return addLineNumbers(html);
|
|
34
|
+
return html;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error('Highlighting error:', e);
|
|
38
|
+
return `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function addLineNumbers(html) {
|
|
42
|
+
const codeMatch = html.match(/<code[^>]*>([\s\S]*)<\/code>/);
|
|
43
|
+
if (!codeMatch)
|
|
44
|
+
return html;
|
|
45
|
+
const codeContent = codeMatch[1];
|
|
46
|
+
const codeLines = codeContent.split('\n');
|
|
47
|
+
const numberedLines = codeLines.map((line, i) => {
|
|
48
|
+
return `<span class="line-number">${i + 1}</span>${line}`;
|
|
49
|
+
}).join('\n');
|
|
50
|
+
return html.replace(codeMatch[1], numberedLines);
|
|
51
|
+
}
|
|
52
|
+
function escapeHtml(str) {
|
|
53
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
54
|
+
}
|