Skip to content

Commit

Permalink
Add a feature to export Sandbox as a video
Browse files Browse the repository at this point in the history
  • Loading branch information
baku89 committed Feb 18, 2024
1 parent f1b2f81 commit b6dfc5e
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 127 deletions.
9 changes: 9 additions & 0 deletions docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default defineUserConfig({
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500&family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500;1,600&display=swap',
crossorigin: 'anonymous',
},
],
['link', {rel: 'icon', href: '/logo.svg'}],
Expand All @@ -36,6 +37,14 @@ export default defineUserConfig({
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200',
crossorigin: 'anonymous',
},
],
[
'script',
{
src: 'https://cdn.jsdelivr.net/npm/ccapture.js-npmfixed@1.1.0/build/CCapture.all.min.js',
crossorigin: 'anonymous',
},
],
],
Expand Down
5 changes: 5 additions & 0 deletions docs/.vuepress/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,8 @@ td code {
.sandbox .theme-default-content {
max-width: unset;
}

.sandbox-code {
font-size: 22px;
font-family: var(--font-family-code);
}
135 changes: 9 additions & 126 deletions docs/components/PaveRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

<script lang="ts" setup>
import {throttledWatch, useCssVar, useElementSize} from '@vueuse/core'
import {mat2d, scalar, vec2} from 'linearly'
import {type Path} from 'pave'
import saferEval from 'safer-eval'
import {computed, onMounted, ref, watch} from 'vue'
import {createDrawFunction, setupEvalContextCreator} from './createDrawFunction'
const props = withDefaults(
defineProps<{
code: string
Expand All @@ -27,108 +26,14 @@ const {width: canvasWidth, height: canvasHeight} = useElementSize(canvas)
onMounted(async () => {
context.value = canvas.value?.getContext('2d') ?? null
const {Path, Arc, CubicBezier, Curve} = await import('pave')
const createDrawContext = await setupEvalContextCreator(brandColor)
const evalContext = computed(() => {
if (!context.value) return
if (!context.value) return {}
const ctx = context.value
const stroke = (path: Path, color = '', lineWidth = 1) => {
ctx.fillStyle = 'none'
ctx.strokeStyle = color || brandColor.value
ctx.lineCap = 'round'
ctx.lineWidth = lineWidth
Path.drawToCanvas(path, ctx)
ctx.stroke()
}
const fill = (path: Path, color = '') => {
ctx.strokeStyle = 'none'
ctx.fillStyle = color || brandColor.value
Path.drawToCanvas(path, ctx)
ctx.fill()
}
const dot = (point: vec2, color = '', size = 3) => {
ctx.strokeStyle = 'none'
ctx.fillStyle = color || brandColor.value
Path.drawToCanvas(Path.circle(point, size / 2), ctx)
ctx.fill()
}
const debug = (path: Path, color = '') => {
const lineWidth = 0.5
const vertexSize = 3
ctx.fillStyle = color || brandColor.value
ctx.strokeStyle = color || brandColor.value
ctx.lineCap = 'round'
ctx.lineWidth = lineWidth
for (const curve of path.curves) {
let isFirstVertex = true
for (const {start, point, command, args} of Curve.segments(curve)) {
ctx.lineWidth = lineWidth
// Draw the first vertex
if (isFirstVertex) {
Path.drawToCanvas(Path.circle(start, vertexSize), ctx)
ctx.stroke()
isFirstVertex = false
}
if (command === 'L') {
Path.drawToCanvas(Path.line(start, point), ctx)
} else if (command === 'C') {
const [control1, control2] = args
// Draw handles
ctx.setLineDash([2, 1])
Path.drawToCanvas(Path.line(start, control1), ctx)
ctx.stroke()
Path.drawToCanvas(Path.line(point, control2), ctx)
ctx.stroke()
ctx.setLineDash([])
Path.drawToCanvas(Path.circle(control1, 1), ctx)
ctx.fill()
Path.drawToCanvas(Path.circle(control2, 1), ctx)
ctx.fill()
const bezier = Path.cubicBezier(start, control1, control2, point)
Path.drawToCanvas(bezier, ctx)
} else if (command === 'A') {
let arc = Path.moveTo(Path.empty, start)
arc = Path.arcTo(arc, ...args, point)
Path.drawToCanvas(arc, ctx)
}
ctx.lineWidth = lineWidth
ctx.stroke()
ctx.lineWidth = vertexSize
Path.drawToCanvas(Path.dot(point), ctx)
ctx.stroke()
ctx.font = '7px "IBM Plex Mono"'
ctx.fillText(command[0], ...vec2.add(point, [2, -2]))
}
}
}
return {
Path,
Arc,
CubicBezier,
scalar,
vec2,
mat2d,
stroke,
fill,
dot,
debug,
}
return createDrawContext(ctx)
})
const evalFn = ref<((time: number) => void) | null>(null)
Expand All @@ -137,37 +42,15 @@ onMounted(async () => {
() =>
[
props.code,
canvas.value,
context.value,
evalContext.value,
canvasWidth.value,
canvasHeight.value,
] as const,
([code, canvas, context, width, height]) => {
if (!canvas || !context) return
([code, context, evalContext]) => {
if (!context) return
const dpi = window.devicePixelRatio
canvas.width = width * dpi
canvas.height = height * dpi
const scale = (width * dpi) / 100
try {
const draw = saferEval(
`(time) => {\n${code}\n}`,
evalContext.value
) as unknown as (time: number) => void
evalFn.value = (time: number) => {
context.clearRect(0, 0, canvas.width, canvas.height)
context.resetTransform()
context.transform(...mat2d.fromScaling([scale, scale]))
draw(time)
}
} catch (e) {
evalFn.value = null
// eslint-disable-next-line no-console
console.error(e)
e
}
evalFn.value = createDrawFunction(context, evalContext, code)
},
{immediate: true, throttle: 100}
)
Expand Down
10 changes: 9 additions & 1 deletion docs/components/Sandbox.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import {useLocalStorage, useRafFn} from '@vueuse/core'
import {useLocalStorage, useMagicKeys, useRafFn, whenever} from '@vueuse/core'
import {ref, watch} from 'vue'
import Editor from './Editor.vue'
Expand Down Expand Up @@ -82,6 +82,14 @@ watch(
},
{flush: 'sync'}
)
const keys = useMagicKeys()
const exportKey = keys.shift_cmd_e
whenever(exportKey, async () => {
const {exportVideo} = await import('./exportVideo')
exportVideo(code.value)
})
</script>

<template>
Expand Down
138 changes: 138 additions & 0 deletions docs/components/createDrawFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {mat2d, scalar, vec2} from 'linearly'
import {type Path} from 'pave'
import saferEval from 'safer-eval'
import {Ref} from 'vue'

export async function setupEvalContextCreator(brandColor: Ref<string>) {
const {Path, Arc, CubicBezier, Curve} = await import('pave')

return (ctx: CanvasRenderingContext2D) => {
const stroke = (path: Path, color = '', lineWidth = 1) => {
ctx.fillStyle = 'none'
ctx.strokeStyle = color || brandColor.value
ctx.lineCap = 'round'
ctx.lineWidth = lineWidth
Path.drawToCanvas(path, ctx)
ctx.stroke()
}

const fill = (path: Path, color = '') => {
ctx.strokeStyle = 'none'
ctx.fillStyle = color || brandColor.value
Path.drawToCanvas(path, ctx)
ctx.fill()
}

const dot = (point: vec2, color = '', size = 3) => {
ctx.strokeStyle = 'none'
ctx.fillStyle = color || brandColor.value
Path.drawToCanvas(Path.circle(point, size / 2), ctx)
ctx.fill()
}

const debug = (path: Path, color = '') => {
const lineWidth = 0.5
const vertexSize = 3

ctx.fillStyle = color || brandColor.value

ctx.strokeStyle = color || brandColor.value
ctx.lineCap = 'round'
ctx.lineWidth = lineWidth

for (const curve of path.curves) {
let isFirstVertex = true
for (const {start, point, command, args} of Curve.segments(curve)) {
ctx.lineWidth = lineWidth

// Draw the first vertex
if (isFirstVertex) {
Path.drawToCanvas(Path.circle(start, vertexSize), ctx)
ctx.stroke()
isFirstVertex = false
}

if (command === 'L') {
Path.drawToCanvas(Path.line(start, point), ctx)
} else if (command === 'C') {
const [control1, control2] = args

// Draw handles
ctx.setLineDash([2, 1])
Path.drawToCanvas(Path.line(start, control1), ctx)
ctx.stroke()
Path.drawToCanvas(Path.line(point, control2), ctx)
ctx.stroke()
ctx.setLineDash([])

Path.drawToCanvas(Path.circle(control1, 1), ctx)
ctx.fill()
Path.drawToCanvas(Path.circle(control2, 1), ctx)
ctx.fill()

const bezier = Path.cubicBezier(start, control1, control2, point)
Path.drawToCanvas(bezier, ctx)
} else if (command === 'A') {
let arc = Path.moveTo(Path.empty, start)
arc = Path.arcTo(arc, ...args, point)
Path.drawToCanvas(arc, ctx)
}
ctx.lineWidth = lineWidth
ctx.stroke()

ctx.lineWidth = vertexSize
Path.drawToCanvas(Path.dot(point), ctx)
ctx.stroke()

ctx.font = '7px "IBM Plex Mono"'
ctx.fillText(command[0], ...vec2.add(point, [2, -2]))
}
}
}

return {
Path,
Arc,
CubicBezier,
scalar,
vec2,
mat2d,
stroke,
fill,
dot,
debug,
}
}
}

export function createDrawFunction(
canvasContext: CanvasRenderingContext2D,
evalContext: Record<string, unknown>,
code: string
) {
const dpi = window.devicePixelRatio
const canvas = canvasContext.canvas
const {width, height} = canvas.getBoundingClientRect()
canvas.width = width * dpi
canvas.height = height * dpi
const scale = (width * dpi) / 100

try {
const draw = saferEval(
`(time) => {\n${code}\n}`,
evalContext
) as unknown as (time: number) => void

return (time: number) => {
canvasContext.clearRect(0, 0, canvas.width, canvas.height)
canvasContext.resetTransform()
canvasContext.transform(...mat2d.fromScaling([scale, scale]))
draw(time)
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
return null
e
}
}
Loading

0 comments on commit b6dfc5e

Please sign in to comment.