Skip to content

Commit

Permalink
Merge pull request #27 from jrafaaael/feat/crop
Browse files Browse the repository at this point in the history
feat: crop
  • Loading branch information
jrafaaael authored Jun 11, 2024
2 parents 9c50ab2 + 59c4a88 commit a7f5426
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ These are some features that we want to develop in the short, medium and long te
| ---------------------------------------------- | ------ | ---------------- | ------ | -------------------- | ------ |
| Store videos || Selfie recording || Auto zoom ||
| Upload videos (recording) || Upload audio || Mobile ||
| Crop | | Clips || Auto trim low-volume ||
| Crop | | Clips || Auto trim low-volume ||
| Trim || Layouts || 3D renderer ||
| Multiple export aspect ratios (mobile, square) || Mockups || | |
| | | Subtitles || | |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@vercel/analytics": "^1.2.2",
"cropperjs": "^1.6.2",
"mp4-muxer": "^3.0.2",
"mp4box": "^0.5.2"
},
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/routes/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
appearence
} from './stores/general-appearance.store';
import { zooms } from './stores/zooms.store';
import { crop } from './stores/crop.store';
import { getBlobDuration } from './utils/get-blob-duration';
import Header from './components/header.svelte';
import Toolbox from './components/toolbox/toolbox.svelte';
Expand All @@ -21,6 +22,7 @@
import Seeker from './components/seeker.svelte';
import Trimmer from './components/trimmer.svelte';
import ZoomList from './components/zoom-list.svelte';
import CropDialog from './components/crop.dialog.svelte';
let videoRef: Video;
let paused = true;
Expand All @@ -35,6 +37,8 @@
zooms.load(values?.zooms ?? []);
$background = values?.background ?? DEFAULT_BACKGROUND;
$appearence = values?.appearence ?? DEFAULT_APPEARENCE;
$crop = values?.crop ? values?.crop : null;
if (values?.trimmings) {
$edits = values?.trimmings;
}
Expand Down Expand Up @@ -67,7 +71,8 @@
background: $background,
appearence: $appearence,
zooms: $zooms,
trimmings: $edits
trimmings: $edits,
crop: $crop
};
localStorage.setItem($page.params.id, JSON.stringify(values));
Expand Down Expand Up @@ -112,6 +117,7 @@
>
{secondsToTime(Math.floor($recording?.duration ?? 0))}
</span>
<CropDialog />
</div>
<div class="w-full h-12 px-10 border-b-2 border-b-white/5 flex items-end">
<Timeline />
Expand Down
35 changes: 35 additions & 0 deletions src/routes/[id]/components/crop-frame.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import { onMount } from 'svelte';
import { videoStatus } from '../stores/video-status.store';
import Cropper from './cropper.svelte';
let canvasRef: HTMLCanvasElement;
onMount(() => {
if (!$videoStatus.ref) return;
const ctx = canvasRef.getContext('2d');
ctx?.drawImage(
$videoStatus.ref,
0,
0,
$videoStatus.ref.videoWidth,
$videoStatus.ref.videoHeight
);
});
</script>

<div class="w-full rounded-md relative">
{#if $videoStatus.ref}
<canvas
class="max-w-full max-h-[720px] block"
width={$videoStatus.ref.videoWidth}
height={$videoStatus.ref.videoHeight}
bind:this={canvasRef}
/>
{/if}
{#if canvasRef}
<Cropper img={canvasRef} on:crop />
{/if}
</div>
46 changes: 46 additions & 0 deletions src/routes/[id]/components/crop.dialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import * as Dialog from '$lib/components/dialog';
import { crop, type Crop as ICrop } from '../stores/crop.store';
import Crop from './icons/crop.svelte';
import CropFrame from './crop-frame.svelte';
let cropValue: ICrop | null = null;
</script>

<Dialog.Root>
<Dialog.Trigger
class="py-[5px] px-3 rounded-md text-neutral-300 flex justify-center items-center gap-2 absolute right-10 transition-all hover:bg-white/5 hover:text-neutral-200"
>
<span class="w-4 aspect-square">
<Crop />
</span>
<span>Crop</span>
</Dialog.Trigger>
<Dialog.Content
title="Crop recording"
className={{
dialog:
'w-[90vw] max-w-[1024px] h-fit p-6 bg-neutral-800 border border-white/5 rounded-xl flex flex-col gap-4 fixed left-1/2 top-1/2 z-20 shadow-lg -translate-x-1/2 -translate-y-1/2',
title: 'text-lg font-medium'
}}
>
<Dialog.Overlay slot="overlay" class="fixed inset-0 z-20 bg-black/50 backdrop-blur-sm" />
<CropFrame on:crop={({ detail }) => (cropValue = detail)} />
<div class="flex justify-end gap-2">
<Dialog.Close
class="w-20 py-1 bg-white/5 border border-white/5 rounded-md text-sm text-neutral-50 hover:bg-white/10 hover:border-white/10"
>
Cancel
</Dialog.Close>
<Dialog.Close
class="w-20 py-1 bg-purple-600 rounded-md text-sm text-neutral-50 hover:bg-purple-600/90"
on:m-click={() => {
$crop = cropValue;
cropValue = null;
}}
>
Save
</Dialog.Close>
</div>
</Dialog.Content>
</Dialog.Root>
27 changes: 27 additions & 0 deletions src/routes/[id]/components/cropper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.min.css';
export let img: HTMLImageElement | HTMLCanvasElement;
const dispatch = createEventDispatcher();
let cropper: Cropper | null = null;
onMount(() => {
cropper = new Cropper(img as HTMLImageElement, {
dragMode: 'none',
viewMode: 2,
autoCropArea: 1,
background: false,
movable: false,
rotatable: false,
scalable: false,
zoomable: false,
crop(event) {
const { detail } = event;
dispatch('crop', { ...detail });
}
});
});
</script>
12 changes: 12 additions & 0 deletions src/routes/[id]/components/icons/crop.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-crop"
><path d="M6 2v14a2 2 0 0 0 2 2h14" />
<path d="M18 22V8a2 2 0 0 0-2-2H2" />
</svg>
28 changes: 22 additions & 6 deletions src/routes/[id]/components/video.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { videoStatus } from '../stores/video-status.store';
import { background } from '../stores/background.store';
import { appearence } from '../stores/general-appearance.store';
import { crop } from '../stores/crop.store';
import { zooms, currentZoomIndex, currentZoom } from '../stores/zooms.store';
import { createMP4 } from '../utils/create-mp4';
import { lerp } from '../utils/lerp';
Expand Down Expand Up @@ -58,14 +59,14 @@
}
function draw(frame: CanvasImageSource, frameTime: number) {
const VIDEO_NATURAL_WIDTH = videoRef?.videoWidth;
const VIDEO_NATURAL_HEIGHT = videoRef?.videoHeight;
const VIDEO_NATURAL_ASPECT_RATIO = VIDEO_NATURAL_WIDTH / VIDEO_NATURAL_HEIGHT;
const MAX_WIDTH = $crop ? $crop.width : videoRef?.videoWidth;
const MAX_HEIGHT = $crop ? $crop.height : videoRef?.videoHeight;
const ASPECT_RATIO = MAX_WIDTH / MAX_HEIGHT;
const p = $appearence.padding * 4;
const cornerRadius = $appearence.cornerRadius;
const shadow = $appearence.shadow;
const width = Math.min(ctx.canvas.height * VIDEO_NATURAL_ASPECT_RATIO, ctx.canvas.width) - p;
const height = Math.min(width / VIDEO_NATURAL_ASPECT_RATIO, ctx.canvas.height);
const width = Math.min(ctx.canvas.height * ASPECT_RATIO, ctx.canvas.width) - p;
const height = Math.min(width / ASPECT_RATIO, ctx.canvas.height);
const left = (ctx.canvas.width - width) / 2;
const top = (ctx.canvas.height - height) / 2;
let nextZoom = $zooms.at($currentZoomIndex + 1);
Expand Down Expand Up @@ -206,7 +207,17 @@
ctx.clip();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx?.drawImage(frame, leftWithZoom, topWithZoom, widthWithZoom, heightWithZoom);
ctx.drawImage(
frame,
$crop?.x ?? 0,
$crop?.y ?? 0,
MAX_WIDTH,
MAX_HEIGHT,
leftWithZoom,
topWithZoom,
widthWithZoom,
heightWithZoom
);
ctx.restore();
}
Expand Down Expand Up @@ -280,17 +291,22 @@
const unsubscribeVideoStatusStore = videoStatus.subscribe(() => {
currentTime = $videoStatus.currentTime;
});
const unsubscribeCropStore = crop.subscribe(() => {
draw(videoRef, $videoStatus.currentTime);
});
backgroundImageRef.addEventListener('load', () => draw(videoRef, $videoStatus.currentTime), {
signal
});
$videoStatus.currentTime = $edits.startAt;
$videoStatus.ref = videoRef;
return () => {
unsubscribeVideoStatusStore();
unsubscribeBackgroundStore();
unsubscribeAppearenceStore();
unsubscribeZoomStore();
unsubscribeCropStore();
controller.abort();
};
});
Expand Down
10 changes: 10 additions & 0 deletions src/routes/[id]/stores/crop.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { writable } from 'svelte/store';

export interface Crop {
width: number;
height: number;
x: number;
y: number;
}

export const crop = writable<Crop | null>(null);
14 changes: 11 additions & 3 deletions src/routes/[id]/stores/video-status.store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { writable } from 'svelte/store';

export const videoStatus = writable({
currentTime: 0
});
interface VideoStatus {
currentTime: number;
ref: HTMLVideoElement | null;
}

export const DEFAULT_VALUE: VideoStatus = {
currentTime: 0,
ref: null
};

export const videoStatus = writable<VideoStatus>(DEFAULT_VALUE);

0 comments on commit a7f5426

Please sign in to comment.