create-pellicule 0.0.1
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/package.json +22 -0
- package/src/index.js +138 -0
- package/template/Video.vue +97 -0
- package/template/package.json +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-pellicule",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a new Pellicule video project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-pellicule": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"vue",
|
|
11
|
+
"video",
|
|
12
|
+
"rendering",
|
|
13
|
+
"pellicule",
|
|
14
|
+
"create",
|
|
15
|
+
"scaffold"
|
|
16
|
+
],
|
|
17
|
+
"author": "Kelvin Omereshone <kelvin@sailscasts.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'node:util'
|
|
4
|
+
import { resolve, join, dirname } from 'node:path'
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'))
|
|
10
|
+
const VERSION = pkg.version
|
|
11
|
+
|
|
12
|
+
// ANSI colors
|
|
13
|
+
const colors = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
bold: '\x1b[1m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
white: '\x1b[37m',
|
|
20
|
+
pellicule: '\x1b[38;2;66;184;131m',
|
|
21
|
+
bgPellicule: '\x1b[48;2;66;184;131m'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const c = {
|
|
25
|
+
error: (s) => `${colors.red}${s}${colors.reset}`,
|
|
26
|
+
info: (s) => `${colors.cyan}${s}${colors.reset}`,
|
|
27
|
+
dim: (s) => `${colors.dim}${s}${colors.reset}`,
|
|
28
|
+
bold: (s) => `${colors.bold}${s}${colors.reset}`,
|
|
29
|
+
highlight: (s) => `${colors.pellicule}${s}${colors.reset}`,
|
|
30
|
+
brand: (s) => `${colors.bgPellicule}${colors.white}${colors.bold}${s}${colors.reset}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
${c.bold('create-pellicule')} ${c.dim(`v${VERSION}`)} - Scaffold a new Pellicule project
|
|
35
|
+
|
|
36
|
+
${c.bold('USAGE')}
|
|
37
|
+
${c.highlight('npm create pellicule')} ${c.dim('→ create in current directory')}
|
|
38
|
+
${c.highlight('npm create pellicule')} <name> ${c.dim('→ create in new directory')}
|
|
39
|
+
|
|
40
|
+
${c.bold('OPTIONS')}
|
|
41
|
+
${c.info('--help')} Show this help message
|
|
42
|
+
${c.info('--version')} Show version number
|
|
43
|
+
|
|
44
|
+
${c.bold('EXAMPLES')}
|
|
45
|
+
${c.dim('# Create in current directory')}
|
|
46
|
+
${c.highlight('npm create pellicule')}
|
|
47
|
+
|
|
48
|
+
${c.dim('# Create in a new directory')}
|
|
49
|
+
${c.highlight('npm create pellicule')} my-video
|
|
50
|
+
|
|
51
|
+
${c.dim('Documentation: https://docs.sailscasts.com/pellicule')}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
function copyTemplate(templateDir, targetDir, projectName) {
|
|
55
|
+
const files = readdirSync(templateDir)
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const srcPath = join(templateDir, file)
|
|
59
|
+
const destPath = join(targetDir, file)
|
|
60
|
+
let content = readFileSync(srcPath, 'utf-8')
|
|
61
|
+
|
|
62
|
+
// Replace template variables
|
|
63
|
+
content = content.replace(/\{\{name\}\}/g, projectName)
|
|
64
|
+
|
|
65
|
+
writeFileSync(destPath, content)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const { values, positionals } = parseArgs({
|
|
71
|
+
allowPositionals: true,
|
|
72
|
+
options: {
|
|
73
|
+
help: { type: 'boolean' },
|
|
74
|
+
version: { type: 'boolean' }
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (values.help) {
|
|
79
|
+
console.log(HELP)
|
|
80
|
+
process.exit(0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (values.version) {
|
|
84
|
+
console.log(VERSION)
|
|
85
|
+
process.exit(0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectName = positionals[0] || '.'
|
|
89
|
+
const targetDir = resolve(projectName)
|
|
90
|
+
const isCurrentDir = projectName === '.'
|
|
91
|
+
|
|
92
|
+
console.log()
|
|
93
|
+
console.log(` ${c.brand(' PELLICULE ')} ${c.dim('Create')}`)
|
|
94
|
+
console.log()
|
|
95
|
+
|
|
96
|
+
// Check if directory exists and has files
|
|
97
|
+
if (existsSync(targetDir)) {
|
|
98
|
+
const files = readdirSync(targetDir)
|
|
99
|
+
const hasFiles = files.filter(f => !f.startsWith('.')).length > 0
|
|
100
|
+
if (hasFiles && !isCurrentDir) {
|
|
101
|
+
console.error(c.error(` Error: Directory "${projectName}" already exists and is not empty.\n`))
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
mkdirSync(targetDir, { recursive: true })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Copy template files
|
|
109
|
+
const templateDir = resolve(__dirname, '../template')
|
|
110
|
+
const displayName = isCurrentDir ? 'current directory' : projectName
|
|
111
|
+
|
|
112
|
+
console.log(` ${c.highlight('Scaffolding project')} in ${c.info(displayName)}...`)
|
|
113
|
+
console.log()
|
|
114
|
+
|
|
115
|
+
copyTemplate(templateDir, targetDir, isCurrentDir ? 'my-pellicule-video' : projectName)
|
|
116
|
+
|
|
117
|
+
// Success message
|
|
118
|
+
console.log(` ${c.highlight('Done!')} Created Pellicule project.`)
|
|
119
|
+
console.log()
|
|
120
|
+
console.log(` ${c.bold('Next steps:')}`)
|
|
121
|
+
console.log()
|
|
122
|
+
if (!isCurrentDir) {
|
|
123
|
+
console.log(` ${c.dim('1.')} cd ${projectName}`)
|
|
124
|
+
console.log(` ${c.dim('2.')} npm install`)
|
|
125
|
+
console.log(` ${c.dim('3.')} npx pellicule`)
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` ${c.dim('1.')} npm install`)
|
|
128
|
+
console.log(` ${c.dim('2.')} npx pellicule`)
|
|
129
|
+
}
|
|
130
|
+
console.log()
|
|
131
|
+
console.log(` ${c.dim('Documentation:')} https://docs.sailscasts.com/pellicule`)
|
|
132
|
+
console.log()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((error) => {
|
|
136
|
+
console.error(c.error(`\nError: ${error.message}\n`))
|
|
137
|
+
process.exit(1)
|
|
138
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useFrame, useVideoConfig, interpolate, Easing } from 'pellicule'
|
|
4
|
+
|
|
5
|
+
const frame = useFrame()
|
|
6
|
+
const { width, height, fps, durationInFrames } = useVideoConfig()
|
|
7
|
+
|
|
8
|
+
// Animation: fade in and scale up
|
|
9
|
+
const opacity = computed(() =>
|
|
10
|
+
interpolate(frame.value, [0, 30], [0, 1])
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const scale = computed(() =>
|
|
14
|
+
interpolate(frame.value, [0, 30], [0.8, 1], { easing: Easing.easeOut })
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Animation: slide up the subtitle
|
|
18
|
+
const subtitleY = computed(() =>
|
|
19
|
+
interpolate(frame.value, [20, 50], [40, 0], { easing: Easing.easeOut })
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const subtitleOpacity = computed(() =>
|
|
23
|
+
interpolate(frame.value, [20, 50], [0, 1])
|
|
24
|
+
)
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="video">
|
|
29
|
+
<!-- Main content -->
|
|
30
|
+
<div
|
|
31
|
+
class="content"
|
|
32
|
+
:style="{
|
|
33
|
+
opacity,
|
|
34
|
+
transform: `scale(${scale})`
|
|
35
|
+
}"
|
|
36
|
+
>
|
|
37
|
+
<h1 class="title">Hello, Pellicule!</h1>
|
|
38
|
+
|
|
39
|
+
<p
|
|
40
|
+
class="subtitle"
|
|
41
|
+
:style="{
|
|
42
|
+
opacity: subtitleOpacity,
|
|
43
|
+
transform: `translateY(${subtitleY}px)`
|
|
44
|
+
}"
|
|
45
|
+
>
|
|
46
|
+
Vue-native programmatic video
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Frame counter (helpful for debugging) -->
|
|
51
|
+
<div class="frame-info">
|
|
52
|
+
Frame {{ frame }} / {{ durationInFrames }}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<style scoped>
|
|
58
|
+
.video {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
67
|
+
color: white;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.content {
|
|
71
|
+
text-align: center;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.title {
|
|
75
|
+
font-size: 72px;
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
margin: 0;
|
|
78
|
+
background: linear-gradient(135deg, #42b883 0%, #6ee7a0 100%);
|
|
79
|
+
-webkit-background-clip: text;
|
|
80
|
+
-webkit-text-fill-color: transparent;
|
|
81
|
+
background-clip: text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.subtitle {
|
|
85
|
+
font-size: 32px;
|
|
86
|
+
color: rgba(255, 255, 255, 0.7);
|
|
87
|
+
margin-top: 20px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.frame-info {
|
|
91
|
+
position: absolute;
|
|
92
|
+
bottom: 40px;
|
|
93
|
+
font-size: 16px;
|
|
94
|
+
color: rgba(255, 255, 255, 0.3);
|
|
95
|
+
font-family: monospace;
|
|
96
|
+
}
|
|
97
|
+
</style>
|