Esc
Navigation
Actions
๐ŸŽจ Cycle Theme t
๐Ÿ”„ Toggle Blog/Dev Mode
โฌ†๏ธ Scroll to Top gg
โฌ‡๏ธ Scroll to Bottom G
BIOS v2.1.0
Loading kernel...
Starting services...
Welcome to dev@sandikodev
NORMAL
dev@enigma
โšก 12% โ”‚ ๐Ÿ’พ 4.2G โ”‚ ๐Ÿ“ถ eth0 โ”‚ ๐Ÿ• 00:00 โ”‚ ๐Ÿ“… Mon 01
โ–ฃ nvim โ€” sveltekit-5-canvas-app.md
dev@enigma:~ โฏ cat sveltekit-5-canvas-app.md

SvelteKit 5 + Runes: Framework Terbaik untuk Canvas-Centered Apps

TL;DR

SvelteKit 5 BEST FOR: Canvas apps, drawing tools, image editors, interactive dashboards
Bundle Size: 3x lebih kecil dari React
DX: Terbaik di kelasnya
Runes: Reactivity system baru yang revolutionary

Kenapa SvelteKit 5?

๐ŸŽฏ Perfect untuk Canvas Apps

  1. No Virtual DOM = Direct DOM manipulation = Faster canvas
  2. Smallest Bundle = 80KB vs React 250KB
  3. Runes API = Reactivity lebih powerful & simple
  4. Built-in Animations = Smooth transitions out of the box

Svelte 5 Runes: Game Changer

Apa itu Runes?

Runes adalah reactivity primitives baru di Svelte 5 yang menggantikan $: syntax.

Before (Svelte 4)

<script>
  let count = 0;
  
  // Reactive statement
  $: doubled = count * 2;
  
  // Reactive block
  $: {
    console.log('Count changed:', count);
  }
</script>

<button on:click={() => count++}>
  {count} (doubled: {doubled})
</button>

After (Svelte 5 Runes)

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  
  $effect(() => {
    console.log('Count changed:', count);
  });
</script>

<button onclick={() => count++}>
  {count} (doubled: {doubled})
</button>

Lebih eksplisit, lebih powerful!

Canvas App dengan Svelte 5 Runes

Project Structure

src/
โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ +page.svelte
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ Canvas.svelte
โ”‚   โ”‚   โ”œโ”€โ”€ Toolbar.svelte
โ”‚   โ”‚   โ””โ”€โ”€ LayerPanel.svelte
โ”‚   โ”œโ”€โ”€ stores/
โ”‚   โ”‚   โ””โ”€โ”€ canvas.svelte.ts  # Runes-based store
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ””โ”€โ”€ fabric.ts

Canvas Store dengan Runes

// lib/stores/canvas.svelte.ts
import type { Canvas } from 'fabric';

class CanvasStore {
  canvas = $state<Canvas | null>(null);
  selectedTool = $state<'brush' | 'eraser' | 'select'>('brush');
  brushSize = $state(20);
  brushColor = $state('#00F0FF');
  
  // Derived state
  isDrawing = $derived(this.selectedTool === 'brush' || this.selectedTool === 'eraser');
  
  // Actions
  setCanvas(canvas: Canvas) {
    this.canvas = canvas;
  }
  
  selectTool(tool: typeof this.selectedTool) {
    this.selectedTool = tool;
  }
  
  setBrushSize(size: number) {
    this.brushSize = size;
    if (this.canvas) {
      this.canvas.freeDrawingBrush.width = size;
    }
  }
}

export const canvasStore = new CanvasStore();

Canvas Component

<!-- lib/components/Canvas.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { fabric } from 'fabric';
  import { canvasStore } from '$lib/stores/canvas.svelte';
  
  let canvasEl = $state<HTMLCanvasElement>();
  let fabricCanvas = $state<fabric.Canvas>();
  
  onMount(() => {
    if (!canvasEl) return;
    
    fabricCanvas = new fabric.Canvas(canvasEl, {
      width: 800,
      height: 600,
      backgroundColor: '#fff',
      isDrawingMode: true
    });
    
    canvasStore.setCanvas(fabricCanvas);
    
    return () => fabricCanvas?.dispose();
  });
  
  // Auto-update brush when store changes
  $effect(() => {
    if (!fabricCanvas) return;
    
    fabricCanvas.freeDrawingBrush.width = canvasStore.brushSize;
    fabricCanvas.freeDrawingBrush.color = canvasStore.brushColor;
  });
</script>

<div class="canvas-container">
  <canvas bind:this={canvasEl}></canvas>
</div>

<style>
  .canvas-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background: linear-gradient(135deg, #0A0E27 0%, #1A1F3A 100%);
  }
  
  canvas {
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  }
</style>

Toolbar Component

<!-- lib/components/Toolbar.svelte -->
<script lang="ts">
  import { canvasStore } from '$lib/stores/canvas.svelte';
  
  const tools = [
    { id: 'brush', icon: '๐Ÿ–Œ๏ธ', label: 'Brush' },
    { id: 'eraser', icon: '๐Ÿงน', label: 'Eraser' },
    { id: 'select', icon: '๐Ÿ‘†', label: 'Select' }
  ] as const;
</script>

<aside class="toolbar">
  <div class="tools">
    {#each tools as tool}
      <button
        class:active={canvasStore.selectedTool === tool.id}
        onclick={() => canvasStore.selectTool(tool.id)}
        aria-label={tool.label}
      >
        <span class="icon">{tool.icon}</span>
        <span class="label">{tool.label}</span>
      </button>
    {/each}
  </div>
  
  <div class="controls">
    <label>
      <span>Size: {canvasStore.brushSize}px</span>
      <input 
        type="range" 
        min="5" 
        max="50" 
        bind:value={canvasStore.brushSize}
      />
    </label>
    
    <label>
      <span>Color:</span>
      <input 
        type="color" 
        bind:value={canvasStore.brushColor}
      />
    </label>
  </div>
</aside>

<style>
  .toolbar {
    position: fixed;
    left: 20px;
    top: 50%;
    transform: translateY(-50%);
    background: rgba(20, 24, 41, 0.95);
    backdrop-filter: blur(20px);
    padding: 20px;
    border-radius: 16px;
    border: 1px solid rgba(0, 240, 255, 0.2);
    display: flex;
    flex-direction: column;
    gap: 20px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  }
  
  .tools {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  
  button {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    background: rgba(255, 255, 255, 0.05);
    border: 2px solid transparent;
    border-radius: 12px;
    color: #E8EAED;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  
  button:hover {
    background: rgba(255, 255, 255, 0.1);
    border-color: rgba(0, 240, 255, 0.3);
  }
  
  button.active {
    background: linear-gradient(135deg, #00F0FF 0%, #7B61FF 100%);
    border-color: #00F0FF;
    box-shadow: 0 4px 20px rgba(0, 240, 255, 0.4);
  }
  
  .controls {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 20px;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
  }
  
  label {
    display: flex;
    flex-direction: column;
    gap: 8px;
    color: #B8BFD8;
    font-size: 14px;
  }
  
  input[type="range"] {
    width: 100%;
  }
  
  input[type="color"] {
    width: 100%;
    height: 40px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
  }
</style>

Main Page

<!-- routes/+page.svelte -->
<script lang="ts">
  import Canvas from '$lib/components/Canvas.svelte';
  import Toolbar from '$lib/components/Toolbar.svelte';
</script>

<svelte:head>
  <title>Canvas Studio - SvelteKit 5</title>
</svelte:head>

<main>
  <Toolbar />
  <Canvas />
</main>

<style>
  :global(body) {
    margin: 0;
    padding: 0;
    overflow: hidden;
  }
</style>

Runes API Deep Dive

1. $state() - Reactive State

let count = $state(0);
let user = $state({ name: 'John', age: 30 });

// Deep reactivity by default!
user.age = 31; // Triggers reactivity

2. $derived() - Computed Values

let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);

// Auto-updates when count changes

3. $effect() - Side Effects

let count = $state(0);

$effect(() => {
  console.log('Count is now:', count);
  document.title = `Count: ${count}`;
});

4. $props() - Component Props

<script lang="ts">
  interface Props {
    title: string;
    count?: number;
  }
  
  let { title, count = 0 }: Props = $props();
</script>

<h1>{title}: {count}</h1>

Comparison: React vs Svelte 5

React (Verbose)

import { useState, useEffect, useMemo } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const doubled = useMemo(() => count * 2, [count]);
  
  useEffect(() => {
    console.log('Count:', count);
  }, [count]);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      {count} (doubled: {doubled})
    </button>
  );
}

Svelte 5 (Concise)

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  
  $effect(() => {
    console.log('Count:', count);
  });
</script>

<button onclick={() => count++}>
  {count} (doubled: {doubled})
</button>

50% less code!

Performance Comparison

Bundle Size

FrameworkInitial BundleGzipped
Svelte 580KB25KB โœ…
React 18250KB80KB
Vue 3150KB50KB

Runtime Performance

MetricSvelte 5ReactVue 3
Initial Render1.2ms3.5ms2.8ms
Update0.8ms2.1ms1.5ms
Memory2MB5MB3.5MB

Svelte 5 = 3x faster!

Advanced Canvas Features

Undo/Redo dengan Runes

// lib/stores/history.svelte.ts
class HistoryStore {
  past = $state<string[]>([]);
  future = $state<string[]>([]);
  
  canUndo = $derived(this.past.length > 0);
  canRedo = $derived(this.future.length > 0);
  
  save(state: string) {
    this.past.push(state);
    this.future = [];
  }
  
  undo() {
    if (!this.canUndo) return;
    const state = this.past.pop()!;
    this.future.push(state);
    return state;
  }
  
  redo() {
    if (!this.canRedo) return;
    const state = this.future.pop()!;
    this.past.push(state);
    return state;
  }
}

export const historyStore = new HistoryStore();

Layer Management

// lib/stores/layers.svelte.ts
interface Layer {
  id: string;
  name: string;
  visible: boolean;
  locked: boolean;
}

class LayersStore {
  layers = $state<Layer[]>([
    { id: '1', name: 'Background', visible: true, locked: false }
  ]);
  
  activeLayerId = $state('1');
  
  activeLayer = $derived(
    this.layers.find(l => l.id === this.activeLayerId)
  );
  
  addLayer(name: string) {
    const id = Date.now().toString();
    this.layers.push({ id, name, visible: true, locked: false });
    this.activeLayerId = id;
  }
  
  toggleVisibility(id: string) {
    const layer = this.layers.find(l => l.id === id);
    if (layer) layer.visible = !layer.visible;
  }
}

export const layersStore = new LayersStore();

Why Svelte 5 > React for Canvas?

1. No Virtual DOM

  • Direct DOM manipulation
  • Faster canvas updates
  • Less memory overhead

2. Smaller Bundle

  • 80KB vs 250KB
  • Faster initial load
  • Better mobile performance

3. Better DX

  • Less boilerplate
  • More intuitive
  • Faster development

4. Built-in Features

  • Transitions
  • Animations
  • Scoped CSS

5. Runes = Power

  • Explicit reactivity
  • Better TypeScript support
  • Easier debugging

Real-World Example: Mini Canva

<!-- routes/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { fabric } from 'fabric';
  
  let canvas = $state<fabric.Canvas>();
  let canvasEl = $state<HTMLCanvasElement>();
  
  // State
  let tool = $state<'draw' | 'text' | 'shape'>('draw');
  let color = $state('#00F0FF');
  let brushSize = $state(5);
  
  // Derived
  let isDrawMode = $derived(tool === 'draw');
  
  onMount(() => {
    if (!canvasEl) return;
    
    canvas = new fabric.Canvas(canvasEl, {
      width: 1200,
      height: 800,
      backgroundColor: '#fff'
    });
    
    return () => canvas?.dispose();
  });
  
  // Effects
  $effect(() => {
    if (!canvas) return;
    canvas.isDrawingMode = isDrawMode;
    if (isDrawMode) {
      canvas.freeDrawingBrush.width = brushSize;
      canvas.freeDrawingBrush.color = color;
    }
  });
  
  function addText() {
    if (!canvas) return;
    const text = new fabric.IText('Double click to edit', {
      left: 100,
      top: 100,
      fill: color
    });
    canvas.add(text);
  }
  
  function addRect() {
    if (!canvas) return;
    const rect = new fabric.Rect({
      left: 100,
      top: 100,
      width: 200,
      height: 100,
      fill: color
    });
    canvas.add(rect);
  }
  
  function clear() {
    canvas?.clear();
    canvas?.setBackgroundColor('#fff', canvas.renderAll.bind(canvas));
  }
  
  function download() {
    if (!canvas) return;
    const dataURL = canvas.toDataURL({ format: 'png' });
    const link = document.createElement('a');
    link.download = 'canvas.png';
    link.href = dataURL;
    link.click();
  }
</script>

<div class="app">
  <header>
    <h1>Canvas Studio</h1>
    <div class="tools">
      <button class:active={tool === 'draw'} onclick={() => tool = 'draw'}>
        ๐Ÿ–Œ๏ธ Draw
      </button>
      <button onclick={addText}>
        ๐Ÿ“ Text
      </button>
      <button onclick={addRect}>
        โฌœ Shape
      </button>
      <input type="color" bind:value={color} />
      <input type="range" min="1" max="50" bind:value={brushSize} />
      <button onclick={clear}>๐Ÿ—‘๏ธ Clear</button>
      <button onclick={download}>๐Ÿ’พ Download</button>
    </div>
  </header>
  
  <main>
    <canvas bind:this={canvasEl}></canvas>
  </main>
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background: #0A0E27;
  }
  
  header {
    padding: 20px;
    background: rgba(20, 24, 41, 0.95);
    border-bottom: 1px solid rgba(0, 240, 255, 0.2);
  }
  
  h1 {
    color: #00F0FF;
    margin: 0 0 16px 0;
  }
  
  .tools {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
  }
  
  button {
    padding: 8px 16px;
    background: rgba(255, 255, 255, 0.1);
    border: 2px solid transparent;
    border-radius: 8px;
    color: white;
    cursor: pointer;
    transition: all 0.3s;
  }
  
  button:hover {
    background: rgba(255, 255, 255, 0.2);
  }
  
  button.active {
    background: linear-gradient(135deg, #00F0FF, #7B61FF);
    border-color: #00F0FF;
  }
  
  main {
    flex: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
  }
  
  canvas {
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  }
</style>

Quick Start

# Create SvelteKit 5 project
npm create svelte@latest canvas-app
cd canvas-app

# Install dependencies
npm install
npm install fabric

# Run dev
npm run dev

Kesimpulan

โœ… Gunakan SvelteKit 5 Jika:

  • Canvas/drawing apps
  • Image editors
  • Interactive dashboards
  • Butuh performance maksimal
  • Tim kecil, fokus DX

โœ… Gunakan React Jika:

  • Butuh mature ecosystem
  • Easy hiring
  • Enterprise scale
  • Banyak third-party libraries

๐Ÿ† Winner untuk Canvas App?

SvelteKit 5 dengan Runes!

  • 3x lebih kecil
  • 3x lebih cepat
  • 50% less code
  • Better DX

Resources

Next: Mau artikel tentang migrasi React ke Svelte 5? Drop comment!

Tags: #svelte #sveltekit #svelte5 #runes #canvas #fabricjs #webdev

dev@enigma:~ โฏ
โ–ฃ navigation
โ–ฃ info
type: post
mode: dev ๐Ÿ’ป