Skip to content

Commit 37f2daf

Browse files
committed
[svelte] Drawing
1 parent c499507 commit 37f2daf

File tree

11 files changed

+7010
-3274
lines changed

11 files changed

+7010
-3274
lines changed

screenshots/drawing.png

1.22 KB
Loading

src/cli.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const config = {
6262
choices: [
6363
{title: 'React', value: 'react'},
6464
{title: 'Vanilla', value: 'vanilla'},
65-
{title: 'Svelte (Todo app only)', value: 'svelte'},
65+
{title: 'Svelte', value: 'svelte'},
6666
],
6767
initial: 0,
6868
},
@@ -138,11 +138,6 @@ const config = {
138138
} = answers;
139139
const typescript = language === 'typescript';
140140
const javascript = !typescript;
141-
if (framework === 'svelte' && appType !== 'todos') {
142-
throw new Error(
143-
'Svelte support is currently available only for the Todo app',
144-
);
145-
}
146141
const react = framework === 'react';
147142
const vanilla = framework === 'vanilla';
148143
const svelte = framework === 'svelte';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{includeFile template="client/src/drawing/settingsStore.ts.hbs" output="client/src/settingsStore.ts"}}
2+
{{includeFile template="client/src/drawing/canvasStore.ts.hbs" output="client/src/canvasStore.ts"}}
3+
{{includeFile template="client/src/shared/Loading.svelte.hbs" output="client/src/Loading.svelte"}}
4+
{{includeFile template="client/src/drawing/DrawingControls.svelte.hbs" output="client/src/DrawingControls.svelte"}}
5+
{{includeFile template="client/src/drawing/Canvas.svelte.hbs" output="client/src/Canvas.svelte"}}
6+
7+
<script{{#if typescript}} lang="ts"{{/if}}>
8+
import {onMount} from 'svelte';
9+
import Canvas from './Canvas.svelte';
10+
import DrawingControls from './DrawingControls.svelte';
11+
import Loading from './Loading.svelte';
12+
import {canvasStoreReady} from './canvasStore';
13+
import {settingsStoreReady} from './settingsStore';
14+
15+
let loading = $state(true);
16+
17+
onMount(async () => {
18+
await Promise.all([settingsStoreReady, canvasStoreReady]);
19+
loading = false;
20+
});
21+
</script>
22+
23+
{#if loading}
24+
<Loading />
25+
{:else}
26+
<DrawingControls />
27+
<Canvas />
28+
{/if}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
2+
3+
<script{{#if typescript}} lang="ts"{{/if}}>
4+
import './brushSize.css';
5+
import {onMount} from 'svelte';
6+
import {settingsStore} from './settingsStore';
7+
8+
const getBrushSize = () =>
9+
{{#if typescript}}(settingsStore.getValue('brushSize') as number | undefined) ?? 5{{else}}settingsStore.getValue('brushSize') ?? 5{{/if}};
10+
11+
let size = $state(getBrushSize());
12+
13+
const updateSize = () => {
14+
size = getBrushSize();
15+
};
16+
17+
const handleInput = (event{{#if typescript}}: Event{{/if}}) => {
18+
{{#if typescript}}
19+
const input = event.currentTarget as HTMLInputElement;
20+
const nextSize = parseInt(input.value);
21+
{{else}}
22+
const nextSize = parseInt(event.currentTarget.value);
23+
{{/if}}
24+
settingsStore.setValue('brushSize', nextSize);
25+
size = nextSize;
26+
};
27+
28+
onMount(() => {
29+
settingsStore.addValueListener('brushSize', updateSize);
30+
updateSize();
31+
});
32+
</script>
33+
34+
<div id="brushSize">
35+
<label for="brushSizeSlider">Size:</label>
36+
<input
37+
id="brushSizeSlider"
38+
type="range"
39+
min="1"
40+
max="50"
41+
value={size}
42+
oninput={handleInput}
43+
/>
44+
<span id="brushSizeValue">{size}</span>
45+
</div>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
{{includeFile template="client/src/drawing/canvas.css.hbs" output="client/src/canvas.css"}}
2+
3+
<script{{#if typescript}} lang="ts"{{/if}}>
4+
import './canvas.css';
5+
import {onMount} from 'svelte';
6+
import {getHlcFunctions} from 'tinybase';
7+
import {canvasStore} from './canvasStore';
8+
{{#if typescript}}import type {StrokeRow} from './canvasStore';{{/if}}
9+
import {settingsStore} from './settingsStore';
10+
11+
const [getNextHlc] = getHlcFunctions();
12+
13+
let canvas{{#if typescript}}: HTMLCanvasElement | undefined{{/if}};
14+
let isDrawing = false;
15+
let currentStrokeId{{#if typescript}}: string | null{{/if}} = null;
16+
17+
const getPoint = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
18+
if (!canvas) {
19+
return null;
20+
}
21+
22+
const rect = canvas.getBoundingClientRect();
23+
const clientX = 'touches' in event ? event.touches[0]?.clientX : event.clientX;
24+
const clientY = 'touches' in event ? event.touches[0]?.clientY : event.clientY;
25+
26+
if (clientX == null || clientY == null) {
27+
return null;
28+
}
29+
30+
return {
31+
x: clientX - rect.left,
32+
y: clientY - rect.top,
33+
};
34+
};
35+
36+
const startStroke = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
37+
isDrawing = true;
38+
currentStrokeId = getNextHlc();
39+
40+
const point = getPoint(event);
41+
if (!point || !currentStrokeId) {
42+
return;
43+
}
44+
45+
const brushColor =
46+
{{#if typescript}}(settingsStore.getValue('brushColor') as string | undefined) ?? '#d81b60'{{else}}settingsStore.getValue('brushColor') ?? '#d81b60'{{/if}};
47+
const brushSize =
48+
{{#if typescript}}(settingsStore.getValue('brushSize') as number | undefined) ?? 5{{else}}settingsStore.getValue('brushSize') ?? 5{{/if}};
49+
50+
canvasStore.setRow('strokes', currentStrokeId, {
51+
color: brushColor,
52+
size: brushSize,
53+
points: JSON.stringify([point.x, point.y]),
54+
});
55+
};
56+
57+
const extendStroke = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
58+
if (!isDrawing || !currentStrokeId) {
59+
return;
60+
}
61+
62+
const point = getPoint(event);
63+
if (!point) {
64+
return;
65+
}
66+
67+
const pointsArray = JSON.parse(
68+
{{#if typescript}}(canvasStore.getCell('strokes', currentStrokeId, 'points') as string | undefined) ?? '[]'{{else}}canvasStore.getCell('strokes', currentStrokeId, 'points') ?? '[]'{{/if}},
69+
){{#if typescript}} as number[]{{/if}};
70+
pointsArray.push(point.x, point.y);
71+
canvasStore.setCell('strokes', currentStrokeId, 'points', JSON.stringify(pointsArray));
72+
};
73+
74+
const endStroke = () => {
75+
isDrawing = false;
76+
currentStrokeId = null;
77+
};
78+
79+
const handleTouchStart = (event{{#if typescript}}: TouchEvent{{/if}}) => {
80+
event.preventDefault();
81+
startStroke(event);
82+
};
83+
84+
const handleTouchMove = (event{{#if typescript}}: TouchEvent{{/if}}) => {
85+
event.preventDefault();
86+
extendStroke(event);
87+
};
88+
89+
onMount(() => {
90+
const draw = () => {
91+
if (!canvas) {
92+
return;
93+
}
94+
95+
const ctx = canvas.getContext('2d');
96+
if (!ctx) {
97+
return;
98+
}
99+
100+
ctx.fillStyle = '#111';
101+
ctx.fillRect(0, 0, canvas.width, canvas.height);
102+
103+
canvasStore.getSortedRowIds('strokes').forEach((id) => {
104+
const stroke =
105+
{{#if typescript}}canvasStore.getRow('strokes', id) as StrokeRow{{else}}canvasStore.getRow('strokes', id){{/if}};
106+
if (!stroke?.points) {
107+
return;
108+
}
109+
110+
const pointsArray = JSON.parse(stroke.points){{#if typescript}} as number[]{{/if}};
111+
if (pointsArray.length < 2) {
112+
return;
113+
}
114+
115+
ctx.strokeStyle = stroke.color;
116+
ctx.lineWidth = stroke.size * 2;
117+
ctx.lineCap = 'round';
118+
ctx.lineJoin = 'round';
119+
ctx.beginPath();
120+
ctx.moveTo(pointsArray[0], pointsArray[1]);
121+
for (let i = 2; i < pointsArray.length; i += 2) {
122+
ctx.lineTo(pointsArray[i], pointsArray[i + 1]);
123+
}
124+
ctx.stroke();
125+
});
126+
};
127+
128+
canvasStore.addTablesListener(draw);
129+
draw();
130+
});
131+
</script>
132+
133+
<canvas
134+
bind:this={canvas}
135+
id="drawingCanvas"
136+
width="600"
137+
height="400"
138+
onmousedown={startStroke}
139+
onmousemove={extendStroke}
140+
onmouseup={endStroke}
141+
onmouseleave={endStroke}
142+
ontouchstart={handleTouchStart}
143+
ontouchmove={handleTouchMove}
144+
ontouchend={endStroke}
145+
></canvas>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{{includeFile template="client/src/drawing/colorPicker.css.hbs" output="client/src/colorPicker.css"}}
2+
3+
<script{{#if typescript}} lang="ts"{{/if}}>
4+
import './colorPicker.css';
5+
import {onMount} from 'svelte';
6+
import {settingsStore} from './settingsStore';
7+
8+
const colors = ['#d81b60', '#1976d2', '#388e3c', '#f57c00', '#7b1fa2', '#fff'];
9+
const getCurrentColor = () =>
10+
{{#if typescript}}(settingsStore.getValue('brushColor') as string | undefined) ?? '#d81b60'{{else}}settingsStore.getValue('brushColor') ?? '#d81b60'{{/if}};
11+
12+
let currentColor = $state(getCurrentColor());
13+
14+
const updateActive = () => {
15+
currentColor = getCurrentColor();
16+
};
17+
18+
const setColor = (color{{#if typescript}}: string{{/if}}) => {
19+
settingsStore.setValue('brushColor', color);
20+
};
21+
22+
onMount(() => {
23+
settingsStore.addValueListener('brushColor', updateActive);
24+
updateActive();
25+
});
26+
</script>
27+
28+
<div id="colorPicker">
29+
{#each colors as color (color)}
30+
<button
31+
class="colorBtn"
32+
class:active={currentColor === color}
33+
style={`background: ${color}`}
34+
onclick={() => setColor(color)}
35+
aria-label={`Select ${color} brush`}
36+
title={color}
37+
></button>
38+
{/each}
39+
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
2+
{{includeFile template="client/src/drawing/drawingControls.css.hbs" output="client/src/drawingControls.css"}}
3+
{{includeFile template="client/src/drawing/ColorPicker.svelte.hbs" output="client/src/ColorPicker.svelte"}}
4+
{{includeFile template="client/src/drawing/BrushSize.svelte.hbs" output="client/src/BrushSize.svelte"}}
5+
6+
<script>
7+
import './button.css';
8+
import './drawingControls.css';
9+
import BrushSize from './BrushSize.svelte';
10+
import ColorPicker from './ColorPicker.svelte';
11+
import {canvasStore} from './canvasStore';
12+
13+
const clearStrokes = () => {
14+
canvasStore.delTable('strokes');
15+
};
16+
</script>
17+
18+
<div id="drawingControls">
19+
<ColorPicker />
20+
<BrushSize />
21+
<button class="primary" onclick={clearStrokes}>Clear</button>
22+
</div>

0 commit comments

Comments
 (0)