feat(canvas.js): init

BREAKING CHANGE:
This commit is contained in:
songsenand 2023-07-04 23:51:18 +08:00
parent 8aab83f871
commit 31e8f6bb67
35 changed files with 3813 additions and 17273 deletions

View File

@ -1,38 +1,47 @@
# create-svelte
# Svelte + Vite
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
This template should help get you started developing with Svelte in Vite.
## Creating a project
## Recommended IDE Setup
If you're seeing this, you've probably already done this step. Congrats!
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
```bash
# create a new project in the current directory
npm create svelte@latest
## Need an official Svelte framework?
# create a new project in my-app
npm create svelte@latest my-app
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `checkJs` in the JS template?**
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```js
// store.js
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ABRUS Markdown</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -1,17 +1,32 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

6488
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,20 @@
{
"name": "abrus",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
},
"devDependencies": {
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@types/cookie": "^0.5.1",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"typescript": "^5.0.0",
"vite": "^4.3.6"
},
"type": "module"
"name": "abrus",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.4",
"svelte": "^3.58.0",
"vite": "^4.3.9"
},
"dependencies": {
"bulma": "^0.9.4",
"fabric": "^5.3.0"
}
}

22
src/App.svelte Normal file
View File

@ -0,0 +1,22 @@
<script>
import { onMount } from "svelte";
import { Canvas } from "./lib/canvas";
onMount(() => {
let c = new Canvas('canvas', '你好, Markdown', 10, 10);
c.show_markdown();
c.show_cursor();
});
</script>
<div>
<canvas id="canvas" width="800" height="600" />
</div>
<style>
#canvas {
position: absolute !important;
top: 5px;
left: 5px;
}
</style>

12
src/app.d.ts vendored
View File

@ -1,12 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

Binary file not shown.

171
src/lib/canvas.js Normal file
View File

@ -0,0 +1,171 @@
import { fabric } from "fabric";
class Config {
constructor() {
(this.PADDING_TOP = 10),
(this.PADDING_BOTTOM = 10),
(this.PADDING_LEFT = 10),
(this.PADDING_RIGHT = 10);
}
available_width(page_width) {
return page_width - this.PADDING_LEFT - this.PADDING_RIGHT;
}
page_height(content_height) {
return content_height + this.PADDING_TOP + this.PADDING_BOTTOM;
}
}
const CONFIG = new Config();
class Cursor {
constructor(top, left, height) {
this.top = top;
this.left = left;
this.width = 2;
this.height = height;
this.interval = 1500;
this.color = "#000000";
this.rect = new fabric.Rect({
height: this.height,
width: this.width,
fill: this.color,
selectable: false,
top: this.top,
left: this.left,
hoverCursor: "text",
});
}
set_pos(top, left) {
this.top = top;
this.left = left;
this.rect.set({ top: top, left: left });
}
set_height(height) {
this.height = height;
this.rect.set({ height: this.height });
}
static from_line(line) {
return new this(line.top, line.left, line.height);
}
update_from_line(line) {
this.set_pos(line.top, line.left);
this.set_height(line.height);
return this;
}
}
class BasicBlock {
constructor(text, top, left) {
this.top = top;
this.left = left;
this.text = new fabric.Text(text, {
left: left,
top: top,
hoverCursor: "text",
selectable: false,
fill: "hsl(0, 0%, 29%)",
});
}
static generate(text, top, left, max_width) {
// TODO: impl
let start = 0;
let end = text.length;
text = new fabric.Text(text, {
left: left,
top: top,
hoverCursor: "text",
selectable: false,
fill: "hsl(0, 0%, 29%)",
});
let _gen = () => {
if (text.width > CONFIG.available_width(max_width)) {
end = Math.floor((start + end) / 2);
let t = text.split(start, end);
text.set({ text: t });
}
if (text.width < CONFIG.available_width(max_width)){
end = Math.floor(start + end);
let t = text.split(start, end);
text.set({ text: t });
}
}
}
width() {
return this.text.width;
}
height() {
return this.text.height;
}
update(text, top, left) {
this.text = text;
this.top = top;
this.left = left;
this.text.set({ text: text, top: this.top, left: this.left });
}
}
class Block extends BasicBlock {
constructor(text, top, left) {
super(text, top, left);
}
}
class Line extends BasicBlock {
constructor(text, top, left) {
super(text, top, left);
}
}
class Canvas {
constructor(element_id, markdown_plaintext, cursor_x, cursor_y) {
this.canvas_id = element_id;
this.canvas = new fabric.Canvas("canvas", { selection: false });
this.plaintext = markdown_plaintext;
this.cursor = new Cursor(cursor_x, cursor_y);
this.current_line = new Line("", 10, 10);
this.canvas.on("mouse:down", () => {});
}
parser() {
// TODO:
return [this.plaintext];
}
show_markdown() {
let content = this.parser();
for (let i in content) {
var text_s = new fabric.Text(content[i], {
left: 10,
top: 10,
hoverCursor: "text",
selectable: false,
fill: "hsl(0, 0%, 29%)",
});
this.canvas.add(text_s);
this.current_line.update(content[i], 10, 10);
}
}
show_cursor() {
let self = this;
self.cursor.update_from_line(self.current_line);
console.log(self.cursor.rect);
self.canvas.add(self.cursor.rect);
let on_change = (opacity) => {
opacity -= 1.0 / self.cursor.interval;
self.cursor.rect.set("opacity", Math.round(Math.abs(opacity)));
self.canvas.renderAll();
};
let k = () => {
return fabric.util.animate({
startValue: 1,
endValue: 0,
duration: self.cursor.interval,
onChange: on_change,
onComplete: k,
});
};
k();
}
}
export { Canvas };

View File

@ -1,16 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 30 30">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5229 6.47715 22 12 22C17.5229 22 22 17.5229 22 12C22 6.47715 17.5229 2 12 2ZM0 12C0 5.3726 5.3726 0 12 0C18.6274 0 24 5.3726 24 12C24 18.6274 18.6274 24 12 24C5.3726 24 0 18.6274 0 12Z"
fill="rgba(0,0,0,0.7)"
stroke="none"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.59162 22.7357C9.49492 22.6109 9.49492 21.4986 9.59162 19.399C8.55572 19.4347 7.90122 19.3628 7.62812 19.1833C7.21852 18.9139 6.80842 18.0833 6.44457 17.4979C6.08072 16.9125 5.27312 16.8199 4.94702 16.6891C4.62091 16.5582 4.53905 16.0247 5.84562 16.4282C7.15222 16.8316 7.21592 17.9303 7.62812 18.1872C8.04032 18.4441 9.02572 18.3317 9.47242 18.1259C9.91907 17.9201 9.88622 17.1538 9.96587 16.8503C10.0666 16.5669 9.71162 16.5041 9.70382 16.5018C9.26777 16.5018 6.97697 16.0036 6.34772 13.7852C5.71852 11.5669 6.52907 10.117 6.96147 9.49369C7.24972 9.07814 7.22422 8.19254 6.88497 6.83679C8.11677 6.67939 9.06732 7.06709 9.73672 7.99999C9.73737 8.00534 10.6143 7.47854 12.0001 7.47854C13.386 7.47854 13.8777 7.90764 14.2571 7.99999C14.6365 8.09234 14.94 6.36699 17.2834 6.83679C16.7942 7.79839 16.3844 8.99999 16.6972 9.49369C17.0099 9.98739 18.2372 11.5573 17.4833 13.7852C16.9807 15.2706 15.9927 16.1761 14.5192 16.5018C14.3502 16.5557 14.2658 16.6427 14.2658 16.7627C14.2658 16.9427 14.4942 16.9624 14.8233 17.8058C15.0426 18.368 15.0585 19.9739 14.8708 22.6234C14.3953 22.7445 14.0254 22.8257 13.7611 22.8673C13.2924 22.9409 12.7835 22.9822 12.2834 22.9982C11.7834 23.0141 11.6098 23.0123 10.9185 22.948C10.4577 22.9051 10.0154 22.8343 9.59162 22.7357Z"
fill="rgba(0,0,0,0.7)"
stroke="none"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

8
src/main.js Normal file
View File

@ -0,0 +1,8 @@
import 'bulma/css/bulma.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
})
export default app

View File

@ -1,53 +0,0 @@
<script>
import Header from './Header.svelte';
import './styles.css';
</script>
<div class="app">
<Header />
<main>
<slot />
</main>
<footer>
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
</footer>
</div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 64rem;
margin: 0 auto;
box-sizing: border-box;
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 12px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 12px 0;
}
}
</style>

View File

@ -1,3 +0,0 @@
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,59 +0,0 @@
<script>
import Counter from './Counter.svelte';
import welcome from '$lib/images/svelte-welcome.webp';
import welcome_fallback from '$lib/images/svelte-welcome.png';
</script>
<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>
<section>
<h1>
<span class="welcome">
<picture>
<source srcset={welcome} type="image/webp" />
<img src={welcome_fallback} alt="Welcome" />
</picture>
</span>
to your new<br />SvelteKit app
</h1>
<h2>
try editing <strong>src/routes/+page.svelte</strong>
</h2>
<Counter />
</section>
<style>
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0.6;
}
h1 {
width: 100%;
}
.welcome {
display: block;
position: relative;
width: 100%;
height: 0;
padding: 0 0 calc(100% * 495 / 2048) 0;
}
.welcome img {
position: absolute;
width: 100%;
height: 100%;
top: 0;
display: block;
}
</style>

View File

@ -1,106 +0,0 @@
<script>
import { spring } from 'svelte/motion';
let count = 0;
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
/**
* @param {number} n
* @param {number} m
*/
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
</script>
<div class="counter">
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong>
</div>
</div>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
</div>
<style>
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
font-size: 2rem;
}
.counter button:hover {
background-color: var(--color-bg-1);
}
svg {
width: 25%;
height: 25%;
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: #444;
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--color-theme-1);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
</style>

View File

@ -1,129 +0,0 @@
<script>
import { page } from '$app/stores';
import logo from '$lib/images/svelte-logo.svg';
import github from '$lib/images/github.svg';
</script>
<header>
<div class="corner">
<a href="https://kit.svelte.dev">
<img src={logo} alt="SvelteKit" />
</a>
</div>
<nav>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
<a href="/">Home</a>
</li>
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
<a href="/about">About</a>
</li>
<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
<a href="/sverdle">Sverdle</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg>
</nav>
<div class="corner">
<a href="https://github.com/sveltejs/kit">
<img src={github} alt="GitHub" />
</a>
</div>
</header>
<style>
header {
display: flex;
justify-content: space-between;
}
.corner {
width: 3em;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
svg {
width: 2em;
height: 3em;
display: block;
}
path {
fill: var(--background);
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
li {
position: relative;
height: 100%;
}
li[aria-current='page']::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--color-theme-1);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 0.5rem;
color: var(--color-text);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
color: var(--color-theme-1);
}
</style>

View File

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,26 +0,0 @@
<svelte:head>
<title>About</title>
<meta name="description" content="About this app" />
</svelte:head>
<div class="text-column">
<h1>About this app</h1>
<p>
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
following into your command line and following the prompts:
</p>
<pre>npm create svelte@latest</pre>
<p>
The page you're looking at is purely static HTML, with no client-side interactivity needed.
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading.
</p>
<p>
The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
using it with JavaScript disabled!
</p>
</div>

View File

@ -1,107 +0,0 @@
@import '@fontsource/fira-mono';
:root {
--font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
--color-bg-0: rgb(202, 216, 228);
--color-bg-1: hsl(209, 36%, 86%);
--color-bg-2: hsl(224, 44%, 95%);
--color-theme-1: #ff3e00;
--color-theme-2: #4075a6;
--color-text: rgba(0, 0, 0, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
font-family: var(--font-body);
color: var(--color-text);
}
body {
min-height: 100vh;
margin: 0;
background-attachment: fixed;
background-color: var(--color-bg-1);
background-size: 100vw 100vh;
background-image: radial-gradient(
50% 50% at 50% 50%,
rgba(255, 255, 255, 0.75) 0%,
rgba(255, 255, 255, 0) 100%
),
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
}
h1,
h2,
p {
font-weight: 400;
}
p {
line-height: 1.5;
}
a {
color: var(--color-theme-1);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
font-size: 2rem;
text-align: center;
}
h2 {
font-size: 1rem;
}
pre {
font-size: 16px;
font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em;
overflow-x: auto;
color: var(--color-text);
}
.text-column {
display: flex;
max-width: 48rem;
flex: 0.6;
flex-direction: column;
justify-content: center;
margin: 0 auto;
}
input,
button {
font-size: inherit;
font-family: inherit;
}
button:focus:not(:focus-visible) {
outline: none;
}
@media (min-width: 720px) {
h1 {
font-size: 2.4rem;
}
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

View File

@ -1,70 +0,0 @@
import { fail } from '@sveltejs/kit';
import { Game } from './game';
/** @satisfies {import('./$types').PageServerLoad} */
export const load = ({ cookies }) => {
const game = new Game(cookies.get('sverdle'));
return {
/**
* The player's guessed words so far
*/
guesses: game.guesses,
/**
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
* an exact match, and 'c' means a close match (right letter, wrong place)
*/
answers: game.answers,
/**
* The correct answer, revealed if the game is over
*/
answer: game.answers.length >= 6 ? game.answer : null
};
};
/** @satisfies {import('./$types').Actions} */
export const actions = {
/**
* Modify game state in reaction to a keypress. If client-side JavaScript
* is available, this will happen in the browser instead of here
*/
update: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const key = data.get('key');
const i = game.answers.length;
if (key === 'backspace') {
game.guesses[i] = game.guesses[i].slice(0, -1);
} else {
game.guesses[i] += key;
}
cookies.set('sverdle', game.toString());
},
/**
* Modify game state in reaction to a guessed word. This logic always runs on
* the server, so that people can't cheat by peeking at the JavaScript
*/
enter: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const guess = /** @type {string[]} */ (data.getAll('guess'));
if (!game.enter(guess)) {
return fail(400, { badGuess: true });
}
cookies.set('sverdle', game.toString());
},
restart: async ({ cookies }) => {
cookies.delete('sverdle');
}
};

View File

@ -1,410 +0,0 @@
<script>
import { confetti } from '@neoconfetti/svelte';
import { enhance } from '$app/forms';
import { reduced_motion } from './reduced-motion';
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
/** Whether or not the user has won */
$: won = data.answers.at(-1) === 'xxxxx';
/** The index of the current guess */
$: i = won ? -1 : data.answers.length;
/** Whether the current guess can be submitted */
$: submittable = data.guesses[i]?.length === 5;
/**
* A map of classnames for all letters that have been guessed,
* used for styling the keyboard
* @type {Record<string, 'exact' | 'close' | 'missing'>}
*/
let classnames;
/**
* A map of descriptions for all letters that have been guessed,
* used for adding text for assistive technology (e.g. screen readers)
* @type {Record<string, string>}
*/
let description;
$: {
classnames = {};
description = {};
data.answers.forEach((answer, i) => {
const guess = data.guesses[i];
for (let i = 0; i < 5; i += 1) {
const letter = guess[i];
if (answer[i] === 'x') {
classnames[letter] = 'exact';
description[letter] = 'correct';
} else if (!classnames[letter]) {
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
}
}
});
}
/**
* Modify the game state without making a trip to the server,
* if client-side JavaScript is enabled
* @param {MouseEvent} event
*/
function update(event) {
const guess = data.guesses[i];
const key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');
if (key === 'backspace') {
data.guesses[i] = guess.slice(0, -1);
if (form?.badGuess) form.badGuess = false;
} else if (guess.length < 5) {
data.guesses[i] += key;
}
}
/**
* Trigger form logic in response to a keydown event, so that
* desktop users can use the keyboard to play the game
* @param {KeyboardEvent} event
*/
function keydown(event) {
if (event.metaKey) return;
document
.querySelector(`[data-key="${event.key}" i]`)
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
}
</script>
<svelte:window on:keydown={keydown} />
<svelte:head>
<title>Sverdle</title>
<meta name="description" content="A Wordle clone written in SvelteKit" />
</svelte:head>
<h1 class="visually-hidden">Sverdle</h1>
<form
method="POST"
action="?/enter"
use:enhance={() => {
// prevent default callback from resetting the form
return ({ update }) => {
update({ reset: false });
};
}}
>
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
{#each Array.from(Array(6).keys()) as row (row)}
{@const current = row === i}
<h2 class="visually-hidden">Row {row + 1}</h2>
<div class="row" class:current>
{#each Array.from(Array(5).keys()) as column (column)}
{@const answer = data.answers[row]?.[column]}
{@const value = data.guesses[row]?.[column] ?? ''}
{@const selected = current && column === data.guesses[row].length}
{@const exact = answer === 'x'}
{@const close = answer === 'c'}
{@const missing = answer === '_'}
<div class="letter" class:exact class:close class:missing class:selected>
{value}
<span class="visually-hidden">
{#if exact}
(correct)
{:else if close}
(present)
{:else if missing}
(absent)
{:else}
empty
{/if}
</span>
<input name="guess" disabled={!current} type="hidden" {value} />
</div>
{/each}
</div>
{/each}
</div>
<div class="controls">
{#if won || data.answers.length >= 6}
{#if !won && data.answer}
<p>the answer was "{data.answer}"</p>
{/if}
<button data-key="enter" class="restart selected" formaction="?/restart">
{won ? 'you won :)' : `game over :(`} play again?
</button>
{:else}
<div class="keyboard">
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
<button
on:click|preventDefault={update}
data-key="backspace"
formaction="?/update"
name="key"
value="backspace"
>
back
</button>
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
<div class="row">
{#each row as letter}
<button
on:click|preventDefault={update}
data-key={letter}
class={classnames[letter]}
disabled={data.guesses[i].length === 5}
formaction="?/update"
name="key"
value={letter}
aria-label="{letter} {description[letter] || ''}"
>
{letter}
</button>
{/each}
</div>
{/each}
</div>
{/if}
</div>
</form>
{#if won}
<div
style="position: absolute; left: 50%; top: 30%"
use:confetti={{
particleCount: $reduced_motion ? 0 : undefined,
force: 0.7,
stageWidth: window.innerWidth,
stageHeight: window.innerHeight,
colors: ['#ff3e00', '#40b3ff', '#676778']
}}
/>
{/if}
<style>
form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
flex: 1;
}
.how-to-play {
color: var(--color-text);
}
.how-to-play::before {
content: 'i';
display: inline-block;
font-size: 0.8em;
font-weight: 900;
width: 1em;
height: 1em;
padding: 0.2em;
line-height: 1;
border: 1.5px solid var(--color-text);
border-radius: 50%;
text-align: center;
margin: 0 0.5em 0 0;
position: relative;
top: -0.05em;
}
.grid {
--width: min(100vw, 40vh, 380px);
max-width: var(--width);
align-self: center;
justify-self: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.grid .row {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 0.2rem;
margin: 0 0 0.2rem 0;
}
@media (prefers-reduced-motion: no-preference) {
.grid.bad-guess .row.current {
animation: wiggle 0.5s;
}
}
.grid.playing .row.current {
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
}
.letter {
aspect-ratio: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
text-transform: lowercase;
border: none;
font-size: calc(0.08 * var(--width));
border-radius: 2px;
background: white;
margin: 0;
color: rgba(0, 0, 0, 0.7);
}
.letter.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.letter.exact {
background: var(--color-theme-2);
color: white;
}
.letter.close {
border: 2px solid var(--color-theme-2);
}
.selected {
outline: 2px solid var(--color-theme-1);
}
.controls {
text-align: center;
justify-content: center;
height: min(18vh, 10rem);
}
.keyboard {
--gap: 0.2rem;
position: relative;
display: flex;
flex-direction: column;
gap: var(--gap);
height: 100%;
}
.keyboard .row {
display: flex;
justify-content: center;
gap: 0.2rem;
flex: 1;
}
.keyboard button,
.keyboard button:disabled {
--size: min(8vw, 4vh, 40px);
background-color: white;
color: black;
width: var(--size);
border: none;
border-radius: 2px;
font-size: calc(var(--size) * 0.5);
margin: 0;
}
.keyboard button.exact {
background: var(--color-theme-2);
color: white;
}
.keyboard button.missing {
opacity: 0.5;
}
.keyboard button.close {
border: 2px solid var(--color-theme-2);
}
.keyboard button:focus {
background: var(--color-theme-1);
color: white;
outline: none;
}
.keyboard button[data-key='enter'],
.keyboard button[data-key='backspace'] {
position: absolute;
bottom: 0;
width: calc(1.5 * var(--size));
height: calc(1 / 3 * (100% - 2 * var(--gap)));
text-transform: uppercase;
font-size: calc(0.3 * var(--size));
padding-top: calc(0.15 * var(--size));
}
.keyboard button[data-key='enter'] {
right: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='backspace'] {
left: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='enter']:disabled {
opacity: 0.5;
}
.restart {
width: 100%;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
border: none;
}
.restart:focus,
.restart:hover {
background: var(--color-theme-1);
color: white;
outline: none;
}
@keyframes wiggle {
0% {
transform: translateX(0);
}
10% {
transform: translateX(-2px);
}
30% {
transform: translateX(4px);
}
50% {
transform: translateX(-6px);
}
70% {
transform: translateX(+4px);
}
90% {
transform: translateX(-2px);
}
100% {
transform: translateX(0);
}
}
</style>

View File

@ -1,72 +0,0 @@
import { words, allowed } from './words.server';
export class Game {
/**
* Create a game object from the player's cookie, or initialise a new game
* @param {string | undefined} serialized
*/
constructor(serialized = undefined) {
if (serialized) {
const [index, guesses, answers] = serialized.split('-');
this.index = +index;
this.guesses = guesses ? guesses.split(' ') : [];
this.answers = answers ? answers.split(' ') : [];
} else {
this.index = Math.floor(Math.random() * words.length);
this.guesses = ['', '', '', '', '', ''];
this.answers = /** @type {string[]} */ ([]);
}
this.answer = words[this.index];
}
/**
* Update game state based on a guess of a five-letter word. Returns
* true if the guess was valid, false otherwise
* @param {string[]} letters
*/
enter(letters) {
const word = letters.join('');
const valid = allowed.has(word);
if (!valid) return false;
this.guesses[this.answers.length] = word;
const available = Array.from(this.answer);
const answer = Array(5).fill('_');
// first, find exact matches
for (let i = 0; i < 5; i += 1) {
if (letters[i] === available[i]) {
answer[i] = 'x';
available[i] = ' ';
}
}
// then find close matches (this has to happen
// in a second step, otherwise an early close
// match can prevent a later exact match)
for (let i = 0; i < 5; i += 1) {
if (answer[i] === '_') {
const index = available.indexOf(letters[i]);
if (index !== -1) {
answer[i] = 'c';
available[index] = ' ';
}
}
}
this.answers.push(answer.join(''));
return true;
}
/**
* Serialize game state so it can be set as a cookie
*/
toString() {
return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;
}
}

View File

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,95 +0,0 @@
<svelte:head>
<title>How to play Sverdle</title>
<meta name="description" content="How to play Sverdle" />
</svelte:head>
<div class="text-column">
<h1>How to play Sverdle</h1>
<p>
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
word guessing game. To play, enter a five-letter English word. For example:
</p>
<div class="example">
<span class="close">r</span>
<span class="missing">i</span>
<span class="close">t</span>
<span class="missing">z</span>
<span class="exact">y</span>
</div>
<p>
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
<span class="close">t</span>
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
Let's make another guess:
</p>
<div class="example">
<span class="exact">p</span>
<span class="exact">a</span>
<span class="exact">r</span>
<span class="exact">t</span>
<span class="exact">y</span>
</div>
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
<p>
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
impossible to cheat. It uses <code>&lt;form&gt;</code> and cookies to submit data, meaning you can
even play with JavaScript disabled!
</p>
</div>
<style>
span {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 0.8em;
width: 2.4em;
height: 2.4em;
background-color: white;
box-sizing: border-box;
border-radius: 2px;
border-width: 2px;
color: rgba(0, 0, 0, 0.7);
}
.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.close {
border-style: solid;
border-color: var(--color-theme-2);
}
.exact {
background: var(--color-theme-2);
color: white;
}
.example {
display: flex;
justify-content: flex-start;
margin: 1rem 0;
gap: 0.2rem;
}
.example span {
font-size: 1.4rem;
}
p span {
position: relative;
border-width: 1px;
border-radius: 1px;
font-size: 0.4em;
transform: scale(2) translate(0, -10%);
margin: 0 1em;
}
</style>

View File

@ -1,26 +0,0 @@
import { readable } from 'svelte/store';
import { browser } from '$app/environment';
const reduced_motion_query = '(prefers-reduced-motion: reduce)';
const get_initial_motion_preference = () => {
if (!browser) return false;
return window.matchMedia(reduced_motion_query).matches;
};
export const reduced_motion = readable(get_initial_motion_preference(), (set) => {
if (browser) {
/**
* @param {MediaQueryListEvent} event
*/
const set_reduced_motion = (event) => {
set(event.matches);
};
const media_query_list = window.matchMedia(reduced_motion_query);
media_query_list.addEventListener('change', set_reduced_motion);
return () => {
media_query_list.removeEventListener('change', set_reduced_motion);
};
}
});

File diff suppressed because it is too large Load Diff

2
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,13 +1,7 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@ -1,6 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [sveltekit()]
});
plugins: [svelte()],
})