Quick start
Create the useVideoFrame composable
composables/useVideoFrame.ts
import { ref, onUnmounted, type Ref } from 'vue';
import type { Helios } from '@helios-project/core';
export function useVideoFrame(helios: Helios): Ref<number> {
const frame = ref(helios.getState().currentFrame);
const update = (state: { currentFrame: number }) => {
frame.value = state.currentFrame;
};
const unsubscribe = helios.subscribe(update);
onUnmounted(() => {
unsubscribe();
});
return frame;
}
Create your first animation
App.vue
<script setup lang="ts">
import { Helios } from '@helios-project/core';
import { useVideoFrame } from './composables/useVideoFrame';
// Initialize Helios singleton
const duration = 5;
const fps = 30;
const helios = new Helios({ duration, fps });
helios.bindToDocumentTimeline();
// Expose to window for debugging/player control
if (typeof window !== 'undefined') {
(window as any).helios = helios;
}
const frame = useVideoFrame(helios);
</script>
<template>
<div class="container">
<div
class="box"
:style="{
opacity: Math.min(1, frame / 30),
transform: `scale(${Math.min(1.5, 0.5 + frame / 150)}) rotate(${frame * 2}deg)`
}"
>
Vue DOM
</div>
<div class="info">Frame: {{ frame.toFixed(2) }}</div>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: white;
font-family: sans-serif;
}
.box {
width: 200px;
height: 200px;
background-color: #42b883;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
border-radius: 20px;
box-shadow: 0 0 20px rgba(66, 184, 131, 0.5);
border: 4px solid #35495e;
}
.info {
margin-top: 2rem;
font-size: 1.5rem;
}
</style>
Animation approaches
- DOM animations
- Canvas animations
Animate Vue components using reactive bindings:
<script setup>
import { computed } from 'vue';
import { useVideoFrame } from './composables/useVideoFrame';
const frame = useVideoFrame(helios);
const progress = computed(() => frame.value / (duration * fps));
const styles = computed(() => ({
opacity: Math.min(1, frame.value / 30),
transform: `translateX(${progress.value * 500}px) rotate(${frame.value * 2}deg)`
}));
</script>
<template>
<div :style="styles">
Animated content
</div>
</template>
Use refs and watchers to draw frame-by-frame animations:
<script setup>
import { ref, watch, onMounted } from 'vue';
import { Helios } from '@helios-project/core';
import { useVideoFrame } from './composables/useVideoFrame';
const duration = 5;
const fps = 30;
const helios = new Helios({ duration, fps });
helios.bindToDocumentTimeline();
if (typeof window !== 'undefined') {
window.helios = helios;
}
const canvasRef = ref(null);
const frame = useVideoFrame(helios);
const draw = () => {
const canvas = canvasRef.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
// Clear
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, width, height);
const time = frame.value / fps * 1000;
const progress = (time % (duration * 1000)) / (duration * 1000);
// Draw Vue-y visuals
const cx = width / 2;
const cy = height / 2;
const size = 200;
ctx.save();
ctx.translate(cx, cy);
// Pulse effect
const scale = 1 + Math.sin(progress * Math.PI * 2) * 0.1;
ctx.scale(scale, scale);
// Rotate
ctx.rotate(progress * Math.PI * 2);
// Vue Triangle (Outer)
ctx.beginPath();
ctx.moveTo(0, size / 2);
ctx.lineTo(size / 2, -size / 2);
ctx.lineTo(-size / 2, -size / 2);
ctx.closePath();
ctx.fillStyle = '#42b883'; // Vue Green
ctx.fill();
// Vue Triangle (Inner)
ctx.beginPath();
ctx.moveTo(0, size / 4);
ctx.lineTo(size / 4, -size / 2);
ctx.lineTo(-size / 4, -size / 2);
ctx.closePath();
ctx.fillStyle = '#35495e'; // Vue Dark Blue
ctx.fill();
ctx.restore();
// Text
ctx.fillStyle = 'white';
ctx.font = '24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`Frame: ${frame.value}`, cx, cy + size / 1.5 + 50);
};
// React to frame changes
watch(frame, draw);
// Handle resize
onMounted(() => {
const canvas = canvasRef.value;
if (!canvas) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
};
window.addEventListener('resize', resize);
resize();
});
</script>
<template>
<canvas ref="canvasRef" style="width: 100%; height: 100%; display: block;"></canvas>
</template>
Animation helpers
Helios provides utility functions for common animation patterns:<script setup>
import { computed } from 'vue';
import { interpolate, spring } from '@helios-project/core';
import { useVideoFrame } from './composables/useVideoFrame';
const frame = useVideoFrame(helios);
// Interpolate x position: 0 -> 200 over frames 0-60
const x = computed(() =>
interpolate(frame.value, [0, 60], [0, 200], { extrapolateRight: 'clamp' })
);
// Spring scale: 0 -> 1 starting at frame 0
const scale = computed(() =>
spring({
frame: frame.value,
fps: 30,
from: 0,
to: 1,
config: { stiffness: 100 }
})
);
</script>
<template>
<div :style="{
transform: `translateX(${x}px) scale(${scale})`,
width: '100px',
height: '100px',
background: 'hotpink'
}">
Animated
</div>
</template>
Sequencing components
Sequence component
Create time-based sequences that show/hide content:components/Sequence.vue
<script setup>
import { computed, inject, provide } from 'vue';
import { sequence } from '@helios-project/core';
const props = defineProps({
from: { type: Number, default: 0 },
durationInFrames: { type: Number, required: true }
});
const parentFrame = inject('videoFrame', 0);
const sequenceState = computed(() =>
sequence({
frame: parentFrame.value,
from: props.from,
durationInFrames: props.durationInFrames
})
);
const isActive = computed(() => sequenceState.value.isActive);
const relativeFrame = computed(() => sequenceState.value.relativeFrame);
provide('videoFrame', relativeFrame);
</script>
<template>
<div v-if="isActive">
<slot />
</div>
</template>
Series component
Automatically sequence child components one after another:components/Series.vue
<script setup>
import { useSlots, h, computed } from 'vue';
const slots = useSlots();
const children = computed(() => {
if (!slots.default) return [];
let currentFrom = 0;
return slots.default().map((vnode) => {
const duration = vnode.props?.durationInFrames || 0;
const newVnode = h(vnode, { from: currentFrom });
currentFrom += duration;
return newVnode;
});
});
</script>
<template>
<component :is="child" v-for="(child, i) in children" :key="i" />
</template>
Usage example
<script setup>
import { ref, provide, onUnmounted } from 'vue';
import { Helios } from '@helios-project/core';
import Sequence from './components/Sequence.vue';
import Series from './components/Series.vue';
const helios = new Helios({ duration: 5, fps: 30 });
helios.bindToDocumentTimeline();
if (typeof window !== 'undefined') {
window.helios = helios;
}
const frame = ref(0);
const unsubscribe = helios.subscribe((state) => {
frame.value = state.currentFrame;
});
onUnmounted(() => {
unsubscribe();
});
provide('videoFrame', frame);
</script>
<template>
<div class="container">
<h1>Vue Animation Helpers</h1>
<div>Root Frame: {{ frame.toFixed(2) }}</div>
<Series>
<!-- Sequence 1: 0-30 frames -->
<Sequence :durationInFrames="30">
<div class="box red">Seq 1</div>
</Sequence>
<!-- Sequence 2: 30-60 frames -->
<Sequence :durationInFrames="30">
<div class="box blue">Seq 2</div>
</Sequence>
</Series>
</div>
</template>
Best practices
Initialize Helios in setup
Initialize Helios in setup
Create the Helios instance in the
<script setup> block to ensure it’s only created once:<script setup>
import { Helios } from '@helios-project/core';
// Good: Single instance per component mount
const helios = new Helios({ duration: 5, fps: 30 });
helios.bindToDocumentTimeline();
</script>
Use computed for derived values
Use computed for derived values
Leverage Vue’s computed properties for animation calculations:
<script setup>
import { computed } from 'vue';
const frame = useVideoFrame(helios);
const progress = computed(() => frame.value / (duration * fps));
const rotation = computed(() => progress.value * 360);
</script>
Clean up subscriptions
Clean up subscriptions
Always unsubscribe in
onUnmounted to prevent memory leaks:<script setup>
import { onUnmounted } from 'vue';
const unsubscribe = helios.subscribe(update);
onUnmounted(() => {
unsubscribe();
});
</script>
Use provide/inject for frame context
Use provide/inject for frame context
Share frame state across component trees using Vue’s dependency injection:
<!-- Parent -->
<script setup>
import { provide } from 'vue';
provide('videoFrame', frame);
</script>
<!-- Child -->
<script setup>
import { inject } from 'vue';
const frame = inject('videoFrame');
</script>
TypeScript support
Helios provides full TypeScript support for Vue 3:import type { Helios } from '@helios-project/core';
import type { Ref } from 'vue';
export function useVideoFrame(helios: Helios): Ref<number> {
const frame = ref<number>(helios.getState().currentFrame);
// ...
return frame;
}
Next steps
Animation helpers
Learn about interpolation, spring physics, and easing functions
Canvas rendering
Create high-performance canvas animations
Sequences
Build complex multi-scene animations
Export videos
Render your animations to video files