[READ-ONLY] Mirror of https://github.com/flo-bit/jazz-endless-canvas. infinite multiplayer drawing canvas in the browser, jazz, svelte, paper.js flo-bit.dev/jazz-endless-canvas/
0

Configure Feed

Select the types of activity you want to include in your feed.

commit

+598 -250
+171 -33
package-lock.json
··· 1 1 { 2 - "name": "jazz-drawings2", 2 + "name": "jazz-endless-canvas", 3 3 "version": "0.0.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 - "name": "jazz-drawings2", 8 + "name": "jazz-endless-canvas", 9 9 "version": "0.0.1", 10 10 "license": "MIT", 11 11 "dependencies": { ··· 15 15 "@sveltejs/adapter-static": "^3.0.8", 16 16 "@use-gesture/vanilla": "^10.3.1", 17 17 "jazz-inspector-element": "^0.14.0", 18 - "jazz-svelte": "^0.14.0", 19 - "jazz-tools": "^0.14.0", 18 + "jazz-svelte": "^0.14.4", 19 + "jazz-tools": "^0.14.4", 20 20 "paper": "^0.12.18" 21 21 }, 22 22 "devDependencies": { ··· 2222 2222 } 2223 2223 }, 2224 2224 "node_modules/cojson-storage": { 2225 - "version": "0.14.0", 2226 - "resolved": "https://registry.npmjs.org/cojson-storage/-/cojson-storage-0.14.0.tgz", 2227 - "integrity": "sha512-DA7fhc4f95ysp8VMX8jAs+si1W55tc1RWAg+QK34ZBwXgheBpyjAAQ/bJewHm7gqvPKJo8B76NBsVscsm7T/xA==", 2225 + "version": "0.14.1", 2226 + "resolved": "https://registry.npmjs.org/cojson-storage/-/cojson-storage-0.14.1.tgz", 2227 + "integrity": "sha512-65VQpJpON5wl6XivkJLtMxW5MRv2+NFCbqxuJDNkP98F4UTTvQoVWzDkgMqOkDwizPS9UNbSnZ/N7i42Cw+cmA==", 2228 2228 "license": "MIT", 2229 2229 "dependencies": { 2230 - "cojson": "0.14.0" 2230 + "cojson": "0.14.1" 2231 2231 } 2232 2232 }, 2233 2233 "node_modules/cojson-storage-indexeddb": { 2234 - "version": "0.14.0", 2235 - "resolved": "https://registry.npmjs.org/cojson-storage-indexeddb/-/cojson-storage-indexeddb-0.14.0.tgz", 2236 - "integrity": "sha512-wpfgdIbiLH1rXJqEMTV9wgQ+v6YLZ41kDS5hVYIUaacFNt3GBQO24xMl4L34m+UK2tqxKvza8EAXqeUpD3C53g==", 2234 + "version": "0.14.1", 2235 + "resolved": "https://registry.npmjs.org/cojson-storage-indexeddb/-/cojson-storage-indexeddb-0.14.1.tgz", 2236 + "integrity": "sha512-yCX72QWaAII0Hb4BBq+mF2pUUfUCjy/C8c6e8Zk2Vz3G1O4dC1hV2o+WZokVpBXhddClPvEHzUaSpeIQMTNB0w==", 2237 + "license": "MIT", 2238 + "dependencies": { 2239 + "cojson": "0.14.1", 2240 + "cojson-storage": "0.14.1" 2241 + } 2242 + }, 2243 + "node_modules/cojson-storage-indexeddb/node_modules/cojson": { 2244 + "version": "0.14.1", 2245 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 2246 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 2247 + "license": "MIT", 2248 + "dependencies": { 2249 + "@noble/ciphers": "^1.3.0", 2250 + "@noble/curves": "^1.9.1", 2251 + "@noble/hashes": "^1.8.0", 2252 + "@opentelemetry/api": "^1.9.0", 2253 + "@scure/base": "1.2.1", 2254 + "jazz-crypto-rs": "0.0.7", 2255 + "neverthrow": "^7.0.1", 2256 + "queueueue": "^4.1.2", 2257 + "unicode-segmenter": "^0.12.0" 2258 + } 2259 + }, 2260 + "node_modules/cojson-storage/node_modules/cojson": { 2261 + "version": "0.14.1", 2262 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 2263 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 2237 2264 "license": "MIT", 2238 2265 "dependencies": { 2239 - "cojson": "0.14.0", 2240 - "cojson-storage": "0.14.0" 2266 + "@noble/ciphers": "^1.3.0", 2267 + "@noble/curves": "^1.9.1", 2268 + "@noble/hashes": "^1.8.0", 2269 + "@opentelemetry/api": "^1.9.0", 2270 + "@scure/base": "1.2.1", 2271 + "jazz-crypto-rs": "0.0.7", 2272 + "neverthrow": "^7.0.1", 2273 + "queueueue": "^4.1.2", 2274 + "unicode-segmenter": "^0.12.0" 2241 2275 } 2242 2276 }, 2243 2277 "node_modules/cojson-transport-ws": { 2244 - "version": "0.14.0", 2245 - "resolved": "https://registry.npmjs.org/cojson-transport-ws/-/cojson-transport-ws-0.14.0.tgz", 2246 - "integrity": "sha512-PhM15u2pxQ6vVsW9sn9ZfTPLOO6W61peMXv+4nRiewZVZ2z3VsspMcxhiFE7QUxD3d0KGK37U5eUcXEwcjhPLg==", 2278 + "version": "0.14.1", 2279 + "resolved": "https://registry.npmjs.org/cojson-transport-ws/-/cojson-transport-ws-0.14.1.tgz", 2280 + "integrity": "sha512-GpCBm5oqn0AHbIW/OX/bqVzlcJcAPOunk2mt1JK/u2BcSzebeu0luU1AG54k/0GUxX/ZbrgmdqeOxPi6QWXZbw==", 2247 2281 "license": "MIT", 2248 2282 "dependencies": { 2249 - "cojson": "0.14.0" 2283 + "cojson": "0.14.1" 2284 + } 2285 + }, 2286 + "node_modules/cojson-transport-ws/node_modules/cojson": { 2287 + "version": "0.14.1", 2288 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 2289 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 2290 + "license": "MIT", 2291 + "dependencies": { 2292 + "@noble/ciphers": "^1.3.0", 2293 + "@noble/curves": "^1.9.1", 2294 + "@noble/hashes": "^1.8.0", 2295 + "@opentelemetry/api": "^1.9.0", 2296 + "@scure/base": "1.2.1", 2297 + "jazz-crypto-rs": "0.0.7", 2298 + "neverthrow": "^7.0.1", 2299 + "queueueue": "^4.1.2", 2300 + "unicode-segmenter": "^0.12.0" 2250 2301 } 2251 2302 }, 2252 2303 "node_modules/color-convert": { ··· 3013 3064 "license": "MIT" 3014 3065 }, 3015 3066 "node_modules/jazz-browser": { 3016 - "version": "0.14.0", 3017 - "resolved": "https://registry.npmjs.org/jazz-browser/-/jazz-browser-0.14.0.tgz", 3018 - "integrity": "sha512-V2/rBYB2oVq70MurpkkQ/3Pms0NrebV5Ru4cJfvdl4hvBCQYK1wM0hHDA4HWXq9oLBbs6NsXrmI8BRsEHAWgkg==", 3067 + "version": "0.14.4", 3068 + "resolved": "https://registry.npmjs.org/jazz-browser/-/jazz-browser-0.14.4.tgz", 3069 + "integrity": "sha512-jQFltPwjPYg1S2/aH1ORCdajn7v9NlTPc8eGOonYIGciR25Jra6W1R770DQJDpTZeKtxjyBEH9utydvk7c+iEg==", 3019 3070 "license": "MIT", 3020 3071 "dependencies": { 3021 - "cojson": "0.14.0", 3022 - "cojson-storage-indexeddb": "0.14.0", 3023 - "cojson-transport-ws": "0.14.0", 3024 - "jazz-tools": "0.14.0" 3072 + "cojson": "0.14.1", 3073 + "cojson-storage-indexeddb": "0.14.1", 3074 + "cojson-transport-ws": "0.14.1", 3075 + "jazz-tools": "0.14.4" 3076 + } 3077 + }, 3078 + "node_modules/jazz-browser/node_modules/cojson": { 3079 + "version": "0.14.1", 3080 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 3081 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 3082 + "license": "MIT", 3083 + "dependencies": { 3084 + "@noble/ciphers": "^1.3.0", 3085 + "@noble/curves": "^1.9.1", 3086 + "@noble/hashes": "^1.8.0", 3087 + "@opentelemetry/api": "^1.9.0", 3088 + "@scure/base": "1.2.1", 3089 + "jazz-crypto-rs": "0.0.7", 3090 + "neverthrow": "^7.0.1", 3091 + "queueueue": "^4.1.2", 3092 + "unicode-segmenter": "^0.12.0" 3025 3093 } 3026 3094 }, 3027 3095 "node_modules/jazz-crypto-rs": { ··· 3055 3123 "react-dom": "^18.3.1" 3056 3124 } 3057 3125 }, 3126 + "node_modules/jazz-inspector-element/node_modules/jazz-tools": { 3127 + "version": "0.14.0", 3128 + "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.14.0.tgz", 3129 + "integrity": "sha512-ioshDuFQtZTz0ovSG0B0osVXPxW/s0XFIiFPheRd2bKWkJAChyRFsIegG6XI9FWnsmiTX8/PhtJ74yXik04gXQ==", 3130 + "license": "MIT", 3131 + "dependencies": { 3132 + "@scure/bip39": "^1.3.0", 3133 + "cojson": "0.14.0", 3134 + "fast-myers-diff": "^3.2.0", 3135 + "zod": "3.25.0-beta.20250518T002810" 3136 + } 3137 + }, 3138 + "node_modules/jazz-inspector/node_modules/jazz-tools": { 3139 + "version": "0.14.0", 3140 + "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.14.0.tgz", 3141 + "integrity": "sha512-ioshDuFQtZTz0ovSG0B0osVXPxW/s0XFIiFPheRd2bKWkJAChyRFsIegG6XI9FWnsmiTX8/PhtJ74yXik04gXQ==", 3142 + "license": "MIT", 3143 + "dependencies": { 3144 + "@scure/bip39": "^1.3.0", 3145 + "cojson": "0.14.0", 3146 + "fast-myers-diff": "^3.2.0", 3147 + "zod": "3.25.0-beta.20250518T002810" 3148 + } 3149 + }, 3058 3150 "node_modules/jazz-react-core": { 3059 3151 "version": "0.14.0", 3060 3152 "resolved": "https://registry.npmjs.org/jazz-react-core/-/jazz-react-core-0.14.0.tgz", ··· 3069 3161 "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" 3070 3162 } 3071 3163 }, 3072 - "node_modules/jazz-svelte": { 3164 + "node_modules/jazz-react-core/node_modules/jazz-tools": { 3073 3165 "version": "0.14.0", 3074 - "resolved": "https://registry.npmjs.org/jazz-svelte/-/jazz-svelte-0.14.0.tgz", 3075 - "integrity": "sha512-8GZ6eTKi3gtlIb8hlIeTgZyX+Y872tpoUDdIxSmHXHJRRsO+0KD+cFIqkIxOPv7uCqT2eZgnTQi47YcWInsHJw==", 3166 + "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.14.0.tgz", 3167 + "integrity": "sha512-ioshDuFQtZTz0ovSG0B0osVXPxW/s0XFIiFPheRd2bKWkJAChyRFsIegG6XI9FWnsmiTX8/PhtJ74yXik04gXQ==", 3168 + "license": "MIT", 3076 3169 "dependencies": { 3170 + "@scure/bip39": "^1.3.0", 3077 3171 "cojson": "0.14.0", 3078 - "jazz-browser": "0.14.0", 3079 - "jazz-tools": "0.14.0" 3172 + "fast-myers-diff": "^3.2.0", 3173 + "zod": "3.25.0-beta.20250518T002810" 3174 + } 3175 + }, 3176 + "node_modules/jazz-svelte": { 3177 + "version": "0.14.4", 3178 + "resolved": "https://registry.npmjs.org/jazz-svelte/-/jazz-svelte-0.14.4.tgz", 3179 + "integrity": "sha512-Pj7GzN3CyAYOG84D2weAOg6RCIKEeLJXMcyVIhzuRQybOQH5LBLGsQM5mz3k6BMAsVJ0Lya4nPbk6GNqbq7AWg==", 3180 + "dependencies": { 3181 + "cojson": "0.14.1", 3182 + "jazz-browser": "0.14.4", 3183 + "jazz-tools": "0.14.4" 3080 3184 }, 3081 3185 "peerDependencies": { 3082 3186 "svelte": "^5.0.0" 3083 3187 } 3084 3188 }, 3189 + "node_modules/jazz-svelte/node_modules/cojson": { 3190 + "version": "0.14.1", 3191 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 3192 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 3193 + "license": "MIT", 3194 + "dependencies": { 3195 + "@noble/ciphers": "^1.3.0", 3196 + "@noble/curves": "^1.9.1", 3197 + "@noble/hashes": "^1.8.0", 3198 + "@opentelemetry/api": "^1.9.0", 3199 + "@scure/base": "1.2.1", 3200 + "jazz-crypto-rs": "0.0.7", 3201 + "neverthrow": "^7.0.1", 3202 + "queueueue": "^4.1.2", 3203 + "unicode-segmenter": "^0.12.0" 3204 + } 3205 + }, 3085 3206 "node_modules/jazz-tools": { 3086 - "version": "0.14.0", 3087 - "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.14.0.tgz", 3088 - "integrity": "sha512-ioshDuFQtZTz0ovSG0B0osVXPxW/s0XFIiFPheRd2bKWkJAChyRFsIegG6XI9FWnsmiTX8/PhtJ74yXik04gXQ==", 3207 + "version": "0.14.4", 3208 + "resolved": "https://registry.npmjs.org/jazz-tools/-/jazz-tools-0.14.4.tgz", 3209 + "integrity": "sha512-DmVWpExJTBpoYlJuPCAn/cPv1J25IQFHd69mCvsQUpuDHKYZOIOP2B8PJDS3TQLTUwKwHCbCGenG0NqK2SAFCQ==", 3089 3210 "license": "MIT", 3090 3211 "dependencies": { 3091 3212 "@scure/bip39": "^1.3.0", 3092 - "cojson": "0.14.0", 3213 + "cojson": "0.14.1", 3093 3214 "fast-myers-diff": "^3.2.0", 3094 3215 "zod": "3.25.0-beta.20250518T002810" 3216 + } 3217 + }, 3218 + "node_modules/jazz-tools/node_modules/cojson": { 3219 + "version": "0.14.1", 3220 + "resolved": "https://registry.npmjs.org/cojson/-/cojson-0.14.1.tgz", 3221 + "integrity": "sha512-U3VhymNJqx56mt61UZT6ey6dDp53Qjq2390//ARBg5FW9qpebg03AqvWQjAZoadmO4QVgfLXAzkZzc5NoEFUMg==", 3222 + "license": "MIT", 3223 + "dependencies": { 3224 + "@noble/ciphers": "^1.3.0", 3225 + "@noble/curves": "^1.9.1", 3226 + "@noble/hashes": "^1.8.0", 3227 + "@opentelemetry/api": "^1.9.0", 3228 + "@scure/base": "1.2.1", 3229 + "jazz-crypto-rs": "0.0.7", 3230 + "neverthrow": "^7.0.1", 3231 + "queueueue": "^4.1.2", 3232 + "unicode-segmenter": "^0.12.0" 3095 3233 } 3096 3234 }, 3097 3235 "node_modules/jiti": {
+2 -2
package.json
··· 43 43 "@sveltejs/adapter-static": "^3.0.8", 44 44 "@use-gesture/vanilla": "^10.3.1", 45 45 "jazz-inspector-element": "^0.14.0", 46 - "jazz-svelte": "^0.14.0", 47 - "jazz-tools": "^0.14.0", 46 + "jazz-svelte": "^0.14.4", 47 + "jazz-tools": "^0.14.4", 48 48 "paper": "^0.12.18" 49 49 }, 50 50 "license": "MIT"
+40 -22
src/lib/schema.ts
··· 1 - import { coField, CoMap, CoList } from 'jazz-tools'; 1 + import { co, z } from 'jazz-tools'; 2 2 3 - export class Point extends CoMap { 4 - x = coField.number; 5 - y = coField.number; 6 - } 3 + const Point = z.object({ 4 + x: z.number(), 5 + y: z.number() 6 + }); 7 7 8 - export class Segment extends CoMap { 9 - point = coField.ref(Point); 10 - handleIn = coField.ref(Point); 11 - handleOut = coField.ref(Point); 12 - } 8 + export const Path = co.map({ 9 + segments: z.array( 10 + z.object({ 11 + point: Point, 12 + handleIn: Point, 13 + handleOut: Point 14 + }) 15 + ), 16 + strokeColor: z.string(), 17 + strokeWidth: z.number() 18 + }); 13 19 14 - export class Segments extends CoList.Of(coField.ref(Segment)) {} 20 + export const PaintingPaths = co.list(Path); 15 21 16 - export class Path extends CoMap { 17 - segments = coField.ref(Segments); 18 - strokeColor = coField.string; 19 - strokeWidth = coField.number; 20 - } 22 + export const PaintingCell = co.map({ 23 + paths: PaintingPaths 24 + }); 25 + 26 + export const PaintingCells = co.record(z.string(), PaintingCell); 27 + 28 + export const Painting = co.map({ 29 + cells: PaintingCells 30 + }); 31 + 32 + export const MyAppAccount = co.account({ 33 + profile: co.profile(), 34 + root: co.map({}) 35 + }); 21 36 22 - export class PaintingPaths extends CoList.Of(coField.ref(Path)) {} 37 + export const cellSize = 1000; 23 38 24 - export class Counter extends CoMap { 25 - painting = coField.ref(PaintingPaths); 26 - count = coField.number; 27 - } 39 + export const gridId = (x: number, y: number): string => { 40 + return `${Math.floor(x)}/${Math.floor(y)}`; 41 + }; 28 42 29 - export class Paintings extends CoList.Of(coField.ref(Counter)) {} 43 + export const indexFromGridId = (gridId: string): { x: number; y: number } | undefined => { 44 + if (!gridId?.includes('/')) return undefined; 45 + const [xIndex, yIndex] = gridId.split('/').map(Number); 46 + return { x: xIndex, y: yIndex }; 47 + };
+3 -3
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 - import '../app.css'; 3 2 import { Head, Toaster } from '@fuxui/base'; 4 3 import { JazzProvider } from 'jazz-svelte'; 5 - import { Account } from 'jazz-tools'; 4 + import { MyAppAccount } from '$lib/schema'; 5 + import '../app.css'; 6 6 7 7 let { children } = $props(); 8 8 </script> ··· 11 11 sync={{ 12 12 peer: `wss://cloud.jazz.tools/?key=flo.bit.dev@gmail.com` 13 13 }} 14 - AccountSchema={Account} 14 + AccountSchema={MyAppAccount} 15 15 > 16 16 17 17 {@render children()}
+27 -21
src/routes/+page.svelte
··· 1 1 <script> 2 - import Draw from './Draw.svelte'; 3 2 import { CoState } from 'jazz-svelte'; 4 - import { Counter, PaintingPaths } from '$lib/schema'; 3 + import { 4 + gridId, 5 + Painting, 6 + PaintingCell, 7 + PaintingCells, 8 + PaintingPaths 9 + } from '$lib/schema'; 10 + import { Button, Modal, Prose } from '@fuxui/base'; 11 + import Draw from './Draw.svelte'; 5 12 import { Group } from 'jazz-tools'; 6 - import { Alert, Button, Heading, Modal, Paragraph, Prose } from '@fuxui/base'; 7 13 8 14 // $effect(() => { 9 - // console.log('painting', painting); 15 + // console.log('painting.current', painting.current); 10 16 // if (painting.current === undefined) { 11 17 // const group = Group.create(); 12 18 // group.addMember('everyone', 'writer'); 13 19 14 - // // create new 15 - // let newPainting = Counter.create( 20 + // const id = gridId(0, 0); 21 + // let newPainting = Painting.create( 16 22 // { 17 - // painting: PaintingPaths.create([], group), 18 - // count: 0 23 + // cells: PaintingCells.create( 24 + // { 25 + // [id]: PaintingCell.create( 26 + // { 27 + // paths: PaintingPaths.create([], group) 28 + // }, 29 + // group 30 + // ) 31 + // }, 32 + // group 33 + // ) 19 34 // }, 20 35 // group 21 36 // ); ··· 24 39 // } 25 40 // }); 26 41 42 + // co_zfasJeym9uvhBJ9mKnnaNJAVyjp 43 + 27 44 let painting = $derived( 28 - new CoState(Counter, 'co_zfasJeym9uvhBJ9mKnnaNJAVyjp', { 29 - resolve: { 30 - painting: { 31 - $each: { 32 - segments: { 33 - $each: true 34 - } 35 - } 36 - } 37 - } 38 - }) 45 + new CoState(Painting, 'co_zWoTxwCotTMzdfP4hNSuRKU3X33') 39 46 ); 40 47 41 48 let isOpen = $state(true); 42 49 </script> 43 50 44 - <Draw {painting} /> 51 + <Draw painting={painting.current} /> 45 52 46 53 <Button onclick={() => (isOpen = true)} class="fixed bottom-2 left-2 z-10" variant="secondary"> 47 54 Info ··· 56 63 <h3>Controls:</h3> 57 64 58 65 <ul> 59 - <li>Click "Random point" to jump to a random drawn point in the canvas</li> 60 66 <li> 61 67 Move around by selecting the "move" tool and dragging the canvas around (or use w, a, s, d) 62 68 </li>
+355 -169
src/routes/Draw.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 2 + import { onDestroy, onMount } from 'svelte'; 3 3 import paper from 'paper'; 4 - import { Counter, PaintingPaths, Path, Point, Segment, Segments } from '$lib/schema'; 5 - import { CoState } from 'jazz-svelte'; 6 - import { Group } from 'jazz-tools'; 4 + import { Group, type Loaded } from 'jazz-tools'; 7 5 import { ColorPicker } from '@fuxui/colors'; 8 6 import { 7 + Box, 9 8 Button, 10 9 cn, 11 10 Popover, 12 11 SliderNumber, 13 12 Subheading, 14 - toast, 15 13 ToggleGroup, 16 14 ToggleGroupItem 17 15 } from '@fuxui/base'; 18 - import { base } from '$app/paths'; 16 + import NumberFlow from '@number-flow/svelte'; 19 17 20 18 import { PinchGesture } from '@use-gesture/vanilla'; 19 + import { cellSize, gridId, PaintingCell, PaintingPaths, Path, Painting } from '$lib/schema'; 21 20 22 - let { painting }: { painting: CoState<Counter> } = $props(); 21 + let { painting }: { painting: Loaded<typeof Painting> } = $props(); 23 22 24 - let tool: paper.Tool | null = null; 23 + let moveTool: paper.Tool | null = null; 24 + let drawTool: paper.Tool | null = null; 25 + 25 26 let path: paper.Path | null = null; 26 27 let scope: paper.PaperScope | null = null; 27 - let drawnPaths: { [key: string]: paper.Path } = $state({}); 28 28 let currentColor = $state('#ec4899'); 29 - let subscription = $state(null); 30 29 31 - $effect(() => { 32 - if (subscription || !painting.current?.painting) return; 30 + let loaded = $state(false); 33 31 34 - PaintingPaths.subscribe( 35 - painting.current.painting.id, 32 + let gridData: Record< 33 + string, 34 + { 35 + unsubscribe?: () => void; 36 + drawnPaths?: Record<string, paper.Path>; 37 + group?: paper.Group; 38 + } 39 + > = $state({}); 40 + 41 + function addSubscription(id: string) { 42 + if (!painting || !painting.cells || !painting.cells[id]?.paths || gridData[id]?.unsubscribe) { 43 + return; 44 + } 45 + 46 + const sub = PaintingPaths.subscribe( 47 + painting.cells[id]?.paths.id, 36 48 { 37 49 resolve: { 38 - $each: { 39 - segments: { 40 - $each: { 41 - point: true, 42 - handleIn: true, 43 - handleOut: true 44 - } 45 - } 46 - } 50 + $each: true 47 51 } 48 52 }, 49 53 (painting) => { 50 - // get all paths 51 - for (let path of painting ?? []) { 52 - if (!path || drawnPaths[path.id]) continue; 54 + addPath(painting, id); 55 + } 56 + ); 57 + // console.log('added subscription', id); 58 + 59 + gridData[id] ??= {}; 60 + 61 + if (gridData[id].group) { 62 + gridData[id].group.visible = true; 63 + } 64 + 65 + gridData[id].unsubscribe = sub; 66 + } 67 + 68 + function removeSubscription(id: string) { 69 + if (!gridData[id]?.unsubscribe) return; 70 + 71 + gridData[id].unsubscribe(); 72 + // console.log('removed subscription', id); 73 + gridData[id].unsubscribe = undefined; 74 + 75 + if (gridData[id].group) { 76 + gridData[id].group.visible = false; 77 + } 78 + } 79 + 80 + function addPath(painting: Loaded<typeof PaintingPaths>, id: string) { 81 + loaded = true; 82 + 83 + gridData[id] ??= {}; 84 + gridData[id].drawnPaths ??= {}; 85 + gridData[id].group ??= new paper.Group(); 86 + 87 + let drawnPaths = gridData[id].drawnPaths; 88 + let group = gridData[id].group; 53 89 54 - drawnPaths[path.id] = new paper.Path(); 55 - drawnPaths[path.id].strokeColor = path.strokeColor as unknown as paper.Color; 56 - drawnPaths[path.id].strokeWidth = path.strokeWidth; 90 + for (let path of painting ?? []) { 91 + if (!path || drawnPaths[path.id]) continue; 92 + 93 + drawnPaths[path.id] = new paper.Path(); 94 + drawnPaths[path.id].strokeColor = path.strokeColor as unknown as paper.Color; 95 + drawnPaths[path.id].strokeWidth = path.strokeWidth; 96 + 97 + group.addChild(drawnPaths[path.id]); 57 98 58 - for (let segment of path.segments ?? []) { 59 - if (!segment) continue; 99 + for (let segment of path.segments ?? []) { 100 + if (!segment) continue; 60 101 61 - scope?.activate(); 102 + scope?.activate(); 62 103 63 - let segmentPoint = new paper.Segment({ 64 - point: new paper.Point(segment.point.x, segment.point.y), 65 - handleIn: new paper.Point(segment.handleIn.x, segment.handleIn.y), 66 - handleOut: new paper.Point(segment.handleOut.x, segment.handleOut.y) 67 - }); 104 + let segmentPoint = new paper.Segment({ 105 + point: new paper.Point(segment.point.x, segment.point.y), 106 + handleIn: new paper.Point(segment.handleIn.x, segment.handleIn.y), 107 + handleOut: new paper.Point(segment.handleOut.x, segment.handleOut.y) 108 + }); 68 109 69 - drawnPaths[path.id].add(segmentPoint); 70 - } 71 - } 110 + drawnPaths[path.id].add(segmentPoint); 72 111 } 73 - ); 112 + } 113 + } 114 + 115 + function drawGrid() { 116 + let color = '#27272a' as unknown as paper.Color; 117 + for (let x = -500; x < 500; x++) { 118 + let topPoint = new paper.Point((x * cellSize) / 5, cellSize * -100); 119 + let bottomPoint = new paper.Point((x * cellSize) / 5, cellSize * 100); 120 + let aLine = new paper.Path.Line(topPoint, bottomPoint); 121 + aLine.strokeColor = color; 122 + aLine.strokeWidth = x % 5 === 0 ? 4 : 1; 123 + 124 + let leftPoint = new paper.Point(cellSize * -100, (x * cellSize) / 5); 125 + let rightPoint = new paper.Point(cellSize * 100, (x * cellSize) / 5); 126 + let bLine = new paper.Path.Line(leftPoint, rightPoint); 127 + bLine.strokeColor = color; 128 + bLine.strokeWidth = x % 5 === 0 ? 4 : 1; 129 + } 130 + } 131 + 132 + $effect(() => { 133 + if (!gridData[gridId(0, 0)] && painting && painting.cells) { 134 + updatedPosition(); 135 + } 74 136 }); 75 137 76 - let isDragging = false; 77 - let lastTouch = { x: 0, y: 0 }; 78 - let currentPosition = { x: 0, y: 0 }; 138 + onDestroy(() => { 139 + for (let id in gridData) { 140 + if (gridData[id]?.unsubscribe) { 141 + gridData[id].unsubscribe(); 142 + delete gridData[id]; 143 + } 144 + } 145 + }); 146 + 147 + function getCellBounds() { 148 + let bounds = scope?.view.bounds; 149 + if (!bounds) return; 150 + 151 + let topY = Math.floor(bounds.y / cellSize); 152 + let bottomY = Math.floor((bounds.y + bounds.height) / cellSize); 153 + let leftX = Math.floor(bounds.x / cellSize); 154 + let rightX = Math.floor((bounds.x + bounds.width) / cellSize); 155 + 156 + return { 157 + top: topY, 158 + bottom: bottomY, 159 + left: leftX, 160 + right: rightX 161 + }; 162 + } 163 + 164 + function updatedPosition() { 165 + // get visible bounds 166 + let bounds = getCellBounds(); 167 + if (!bounds) return; 168 + 169 + // console.log('updated position', bounds); 170 + 171 + let visibleCells = new Set<string>(); 172 + for (let x = bounds.left; x <= bounds.right; x++) { 173 + for (let y = bounds.top; y <= bounds.bottom; y++) { 174 + addSubscription(gridId(x, y)); 175 + visibleCells.add(gridId(x, y)); 176 + } 177 + } 178 + 179 + for (let id in gridData) { 180 + if (!visibleCells.has(id)) { 181 + removeSubscription(id); 182 + } 183 + } 184 + 185 + currentX = (scope?.view.center.x ?? 0) / cellSize; 186 + currentY = (scope?.view.center.y ?? 0) / cellSize; 187 + } 188 + 189 + function onZoom(delta: number, origin: number[]) { 190 + if (!scope) return; 191 + 192 + let newZoom = scope.view.zoom; 193 + let oldZoom = scope.view.zoom; 194 + 195 + newZoom = scope.view.zoom + delta; 196 + 197 + newZoom = Math.max(0.1, newZoom); 198 + newZoom = Math.min(5, newZoom); 199 + 200 + let beta = oldZoom / newZoom; 201 + 202 + let mousePosition = new paper.Point(origin[0], origin[1]); 203 + 204 + let viewPosition = scope.view.viewToProject(mousePosition); 79 205 80 - // const handleTouchStart = (event: TouchEvent) => { 81 - // if (event.touches.length === 2) { 82 - // // Start dragging, record initial touch points 83 - // isDragging = true; 84 - // lastTouch.x = event.touches[0].pageX - currentPosition.x; 85 - // lastTouch.y = event.touches[0].pageY - currentPosition.y; 86 - // } 87 - // }; 206 + let mpos = viewPosition; 207 + let ctr = scope.view.center; 88 208 89 - // const handleTouchMove = (event: TouchEvent) => { 90 - // if (isDragging && event.touches.length === 2) { 91 - // // Calculate the new position based on finger movement 92 - // currentPosition.x = event.touches[0].pageX - lastTouch.x; 93 - // currentPosition.y = event.touches[0].pageY - lastTouch.y; 209 + let pc = mpos.subtract(ctr); 210 + let offset = mpos.subtract(pc.multiply(beta)).subtract(ctr); 94 211 95 - // scope?.view.translate(currentPosition); 96 - // } 97 - // }; 212 + scope.view.zoom = newZoom; 213 + scope.view.center = scope.view.center.add(offset); 98 214 99 - // const handleTouchEnd = (event: TouchEvent) => { 100 - // if (event.touches.length < 2) { 101 - // // End dragging when fewer than two fingers are on the screen 102 - // isDragging = false; 103 - // } 104 - // }; 215 + updatedPosition(); 216 + } 105 217 106 218 onMount(async () => { 107 219 if (!canvas) { 108 220 return; 109 221 } 110 222 223 + setInterval(() => { 224 + updatedPosition(); 225 + }, 2000); 226 + 111 227 document.addEventListener('gesturestart', (e) => e.preventDefault()); 112 228 document.addEventListener('gesturechange', (e) => e.preventDefault()); 113 229 114 230 scope = new paper.PaperScope(); 115 231 scope.setup(canvas); 116 - tool = new paper.Tool(); 232 + drawTool = new paper.Tool(); 117 233 118 234 let move = 40; 119 235 window.addEventListener('keydown', (e) => { ··· 140 256 } 141 257 }); 142 258 143 - let startMatrix: paper.Matrix; 144 - let startMatrixInverted: paper.Matrix; 145 - let p0ProjectCoords: paper.Point; 146 - 147 - function getCenterPoint(e: any) { 148 - return new paper.Point(e.origin[0], e.origin[1]); 149 - } 150 - 151 259 const gesture = new PinchGesture(canvas, (e) => { 152 260 isOpen = false; 153 - if(!scope)return; 154 - console.log(e); 155 - 156 - // if (e.first) { 157 - // startMatrix = scope.view.matrix.clone(); 158 - // startMatrixInverted = startMatrix.inverted(); 159 - // const p0 = new paper.Point(e.origin[0], e.origin[1]); 160 - // p0ProjectCoords = scope.view.viewToProject(p0); 161 - // } else if (!e.last) { 162 - // const p = new paper.Point(e.origin[0], e.origin[1]); 163 - // const pProject0 = p.transform(startMatrixInverted); 164 - // let currentScale = scope.view.scaling.x ?? 1; 165 - // const delta = pProject0.subtract(p0ProjectCoords).divide(currentScale + e.delta[0]); 166 - // scope.view.matrix = startMatrix.clone().scale(currentScale + e.delta[0], p0ProjectCoords).translate(delta); 167 - // } 261 + if (!scope) return; 262 + onZoom(e.delta[0], e.origin); 168 263 }); 169 264 170 - tool.onMouseDown = (event: paper.ToolEvent) => { 171 - isOpen = false; 172 - if (event.modifiers.shift || selectedTool === 'move') { 265 + drawTool.onMouseDown = (event: paper.ToolEvent) => { 266 + if (path) { 267 + finishPath(); 173 268 return; 174 269 } 270 + isOpen = false; 175 271 176 272 scope?.activate(); 177 273 ··· 182 278 path.add(event.point); 183 279 }; 184 280 185 - tool.onMouseDrag = (event: paper.ToolEvent) => { 281 + drawTool.onMouseDrag = (event: paper.ToolEvent) => { 186 282 isOpen = false; 187 - if (event.modifiers.shift || selectedTool === 'move') { 188 - scope?.view.translate(event.delta); 189 - return; 190 - } 191 283 192 284 if (path === null) { 193 285 return; ··· 197 289 path.add(event.point); 198 290 }; 199 291 200 - tool.onMouseUp = (event: paper.ToolEvent) => { 201 - if (event.modifiers.shift || path === null) { 202 - return; 203 - } 292 + drawTool.onMouseUp = (event: paper.ToolEvent) => { 293 + finishPath(); 294 + }; 204 295 205 - scope?.activate(); 296 + moveTool = new paper.Tool(); 206 297 207 - path.simplify(10); 298 + moveTool.onMouseDrag = function (event: any) { 299 + if (!scope) return; 208 300 209 - const group = Group.create(); 210 - group.addMember('everyone', 'writer'); 301 + var pan_offset = event.point.subtract(event.downPoint); 302 + scope.view.center = scope.view.center.subtract(pan_offset); 211 303 212 - // add path to painting 213 - let coSegments = Segments.create([], group); 214 - let coPath = Path.create( 215 - { 216 - segments: coSegments, 217 - strokeColor: currentColor, 218 - strokeWidth: strokeWidth 219 - }, 220 - group 221 - ); 222 - for (let segment of path.segments) { 223 - const position = Point.create( 224 - { 225 - x: segment.point.x, 226 - y: segment.point.y 227 - }, 228 - group 229 - ); 230 - const handleIn = Point.create( 231 - { 232 - x: segment.handleIn.x, 233 - y: segment.handleIn.y 234 - }, 235 - group 236 - ); 237 - const handleOut = Point.create( 238 - { 239 - x: segment.handleOut.x, 240 - y: segment.handleOut.y 241 - }, 242 - group 243 - ); 244 - coSegments.push( 245 - Segment.create( 246 - { 247 - point: position, 248 - handleIn: handleIn, 249 - handleOut: handleOut 250 - }, 251 - group 252 - ) 253 - ); 254 - } 304 + updatedPosition(); 305 + }; 255 306 256 - drawnPaths[coPath.id] = path; 257 - 258 - painting.current?.painting?.push(coPath); 259 - if (painting.current) { 260 - painting.current.count += 1; 261 - } 307 + scope.tool = moveTool; 262 308 263 - path = null; 264 - }; 309 + drawGrid(); 265 310 }); 266 311 267 312 let canvas: HTMLCanvasElement | null = null; ··· 277 322 let selectedTool = $state('move'); 278 323 279 324 let isOpen = $state(false); 325 + 326 + function finishPath() { 327 + if (!path) return; 328 + 329 + scope?.activate(); 330 + 331 + path.simplify(10); 332 + 333 + const group = Group.create(); 334 + group.addMember('everyone', 'writer'); 335 + 336 + let coPath = Path.create( 337 + { 338 + segments: path.segments.map((segment) => { 339 + return { 340 + point: { 341 + x: segment.point.x, 342 + y: segment.point.y 343 + }, 344 + handleIn: { 345 + x: segment.handleIn.x, 346 + y: segment.handleIn.y 347 + }, 348 + handleOut: { 349 + x: segment.handleOut.x, 350 + y: segment.handleOut.y 351 + } 352 + }; 353 + }), 354 + strokeColor: currentColor, 355 + strokeWidth: strokeWidth 356 + }, 357 + group 358 + ); 359 + 360 + let gridIdX = Math.floor(path.segments[0].point.x / cellSize); 361 + let gridIdY = Math.floor(path.segments[0].point.y / cellSize); 362 + 363 + let id = gridId(gridIdX, gridIdY); 364 + 365 + gridData[id] ??= {}; 366 + gridData[id].drawnPaths ??= {}; 367 + gridData[id].drawnPaths[coPath.id] = path; 368 + 369 + if (!painting.cells) return; 370 + 371 + if (!painting.cells[id]) { 372 + painting.cells[id] = PaintingCell.create( 373 + { 374 + paths: PaintingPaths.create([], group) 375 + }, 376 + group 377 + ); 378 + } 379 + painting.cells[id]?.paths?.push(coPath); 380 + 381 + path = null; 382 + } 383 + 384 + let currentX = $state(0.0001); 385 + let currentY = $state(0.0001); 280 386 </script> 281 387 282 388 <div class="fixed right-2 bottom-2 z-20 flex flex-col gap-3"></div> ··· 287 393 'fixed h-screen w-screen', 288 394 selectedTool === 'move' ? 'cursor-grab' : 'cursor-crosshair' 289 395 )} 396 + data-paper-resize="true" 290 397 ></canvas> 291 398 292 - <!-- ontouchstart={handleTouchStart} 293 - ontouchmove={handleTouchMove} 294 - ontouchend={handleTouchEnd} --> 295 - 296 - <div class="fixed top-2 left-2 z-20"> 399 + <div class="fixed top-2 left-2 z-20 hidden"> 297 400 <Button 298 401 onclick={() => { 299 - // toast('Link copied to clipboard', { 300 - // description: 'Send this link to your friends to draw together' 301 - // }); 302 - // navigator.clipboard.writeText(window.location.href); 402 + const gridIds = Object.keys(gridData); 403 + const randomGridId = gridIds[Math.floor(Math.random() * gridIds.length)]; 404 + 405 + if (!gridData[randomGridId]) return; 406 + 407 + let drawnPaths = gridData[randomGridId].drawnPaths; 408 + if (!drawnPaths) return; 303 409 304 410 // jump to random drawing 305 411 const numDrawnPaths = Object.keys(drawnPaths).length; ··· 311 417 // scope?.view.center.set(new paper.Point(point.x, point.y)); 312 418 // get current translation 313 419 const translation = scope?.view.center; 314 - console.log(translation); 420 + // console.log(translation); 315 421 if (translation) { 316 422 scope?.view.translate(new paper.Point(translation.x - point.x, translation.y - point.y)); 317 423 ··· 323 429 > 324 430 Random point 325 431 </Button> 432 + 433 + <Button 434 + onclick={() => { 435 + // add 100 random paths 436 + let paths = []; 437 + for (let i = 0; i < 100000; i++) { 438 + const path = new paper.Path(); 439 + path.strokeColor = currentColor as unknown as paper.Color; 440 + path.strokeWidth = strokeWidth; 441 + let rx = Math.random() * 10000 - 5000; 442 + let ry = Math.random() * 10000 - 5000; 443 + path.add(new paper.Point(rx, ry)); 444 + path.add(new paper.Point(rx + 50, ry + 50)); 445 + paths.push(path); 446 + 447 + continue; 448 + 449 + const group = Group.create(); 450 + group.addMember('everyone', 'writer'); 451 + 452 + let coPath = Path.create( 453 + { 454 + segments: path.segments.map((segment) => { 455 + return { 456 + point: { 457 + x: segment.point.x, 458 + y: segment.point.y 459 + }, 460 + handleIn: { 461 + x: segment.handleIn.x, 462 + y: segment.handleIn.y 463 + }, 464 + handleOut: { 465 + x: segment.handleOut.x, 466 + y: segment.handleOut.y 467 + } 468 + }; 469 + }), 470 + strokeColor: currentColor, 471 + strokeWidth: strokeWidth 472 + }, 473 + group 474 + ); 475 + 476 + painting?.paths?.push(coPath); 477 + } 478 + 479 + //create new group 480 + const pGroup = new paper.Group(paths); 481 + }} 482 + > 483 + Add lots 484 + </Button> 326 485 </div> 327 486 328 - <div class="absolute top-2 right-2"> 487 + <div class="fixed top-2 left-2 z-20 p-0"> 488 + <Box class="px-1 py-1"> 489 + <div class="bg-base-800/50 rounded-2xl px-2"> 490 + <NumberFlow 491 + value={currentX} 492 + format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }} 493 + /> / 494 + <NumberFlow 495 + value={currentY} 496 + format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }} 497 + /> 498 + </div> 499 + </Box> 500 + </div> 501 + {#if !painting || !loaded} 502 + <div class="pointer-events-none fixed inset-0 flex h-full w-full items-center justify-center"> 503 + <Subheading>Loading...</Subheading> 504 + </div> 505 + {/if} 506 + 507 + <div class="absolute right-2 bottom-2"> 329 508 <div class="bg-base-900 flex items-center justify-center gap-4 rounded-2xl p-2"> 330 509 <ToggleGroup 331 510 type="single" ··· 334 513 return selectedTool; 335 514 }, 336 515 (value) => { 516 + if (!value) return; 517 + 337 518 selectedTool = value; 519 + if (!scope) return; 520 + if (selectedTool === 'move') { 521 + scope.tool = moveTool!; 522 + } else { 523 + scope.tool = drawTool!; 524 + } 338 525 } 339 526 } 340 527 > ··· 403 590 <ColorPicker 404 591 bind:rgb={color} 405 592 onchange={(color) => { 406 - console.log(color); 407 593 currentColor = color.hex; 408 594 }} 409 595 quickSelects={[