diff --git a/.editorconfig b/.editorconfig index ef8b2d9..0f09989 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,9 +2,9 @@ root = true [*] -indent_style = tab +indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file +insert_final_newline = true diff --git a/playgrounds/app/migrations/0004_optimal_zemo.sql b/playgrounds/app/migrations/0004_optimal_zemo.sql new file mode 100644 index 0000000..029182b --- /dev/null +++ b/playgrounds/app/migrations/0004_optimal_zemo.sql @@ -0,0 +1,4 @@ +ALTER TABLE `snippets_table` ADD `bgType` text DEFAULT 'solid' NOT NULL;--> statement-breakpoint +ALTER TABLE `snippets_table` ADD `bgGradientColorStart` text DEFAULT '#ffffff';--> statement-breakpoint +ALTER TABLE `snippets_table` ADD `bgGradientColorEnd` text DEFAULT '#ffffff';--> statement-breakpoint +ALTER TABLE `snippets_table` ADD `bgGradientDirection` integer DEFAULT 0; \ No newline at end of file diff --git a/playgrounds/app/migrations/meta/0004_snapshot.json b/playgrounds/app/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..5878655 --- /dev/null +++ b/playgrounds/app/migrations/meta/0004_snapshot.json @@ -0,0 +1,319 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "99b25c51-31d1-41bc-9aff-2240829bfa62", + "prevId": "55589ffe-ec55-466e-9854-e59f5c5810b1", + "tables": { + "snippets_table": { + "name": "snippets_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "codeLeft": { + "name": "codeLeft", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "codeRight": { + "name": "codeRight", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "snippetWidth": { + "name": "snippetWidth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 450 + }, + "yPadding": { + "name": "yPadding", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 42 + }, + "xPadding": { + "name": "xPadding", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 42 + }, + "shadowEnabled": { + "name": "shadowEnabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shadowOffsetY": { + "name": "shadowOffsetY", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "shadowBlur": { + "name": "shadowBlur", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "shadowColor": { + "name": "shadowColor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#000000'" + }, + "shadowOpacity": { + "name": "shadowOpacity", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.6 + }, + "bgColor": { + "name": "bgColor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#ffffff'" + }, + "bgType": { + "name": "bgType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'solid'" + }, + "bgGradientColorStart": { + "name": "bgGradientColorStart", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'#ffffff'" + }, + "bgGradientColorEnd": { + "name": "bgGradientColorEnd", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'#ffffff'" + }, + "bgGradientDirection": { + "name": "bgGradientDirection", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tsx'" + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'nord'" + }, + "fontSize": { + "name": "fontSize", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 16 + }, + "fontFamily": { + "name": "fontFamily", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Fira Code'" + } + }, + "indexes": {}, + "foreignKeys": { + "snippets_table_userId_users_table_id_fk": { + "name": "snippets_table_userId_users_table_id_fk", + "tableFrom": "snippets_table", + "tableTo": "users_table", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "githubUsername": { + "name": "githubUsername", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "githubAvatarUrl": { + "name": "githubAvatarUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_table_githubId_unique": { + "name": "users_table_githubId_unique", + "columns": [ + "githubId" + ], + "isUnique": true + }, + "users_table_githubUsername_unique": { + "name": "users_table_githubUsername_unique", + "columns": [ + "githubUsername" + ], + "isUnique": true + }, + "users_table_githubAvatarUrl_unique": { + "name": "users_table_githubAvatarUrl_unique", + "columns": [ + "githubAvatarUrl" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/playgrounds/app/migrations/meta/_journal.json b/playgrounds/app/migrations/meta/_journal.json index af2de8e..95663f9 100644 --- a/playgrounds/app/migrations/meta/_journal.json +++ b/playgrounds/app/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1730069290357, "tag": "0003_futuristic_wiccan", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1730073978913, + "tag": "0004_optimal_zemo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/playgrounds/app/src/app.css b/playgrounds/app/src/app.css index 2cea3a1..753dd98 100644 --- a/playgrounds/app/src/app.css +++ b/playgrounds/app/src/app.css @@ -141,10 +141,13 @@ } -/* Full App Height and Letting Footer in the End */ +/* + Full App Height and Letting Footer in the End + Max Width to the Main Container for Larger Screen Sizes +*/ #app { - @apply min-h-screen flex flex-col ; + @apply min-h-screen flex flex-col; } #app main { - @apply w-full flex-1; + @apply w-full flex-1 max-w-screen-2xl mx-auto; } \ No newline at end of file diff --git a/playgrounds/app/src/components/Editor.tsx b/playgrounds/app/src/components/Editor.tsx index 8ae3a6e..40e2fe9 100644 --- a/playgrounds/app/src/components/Editor.tsx +++ b/playgrounds/app/src/components/Editor.tsx @@ -25,8 +25,11 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuGroupLabel, DropdownMenuItem, DropdownMenuPortal, + DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, @@ -97,8 +100,16 @@ interface EditorProps { setShadowColor: Setter shadowOpacity: number setShadowOpacity: Setter + bgType: 'solid' | 'linearGradient' + setBgType: Setter<'solid' | 'linearGradient'> bgColor: string setBgColor: Setter + bgGradientColorStart: string + setBgGradientColorStart: Setter + bgGradientColorEnd: string + setBgGradientColorEnd: Setter + bgGradientDirection: number + setBgGradientDirection: Setter fontSize: number setFontSize: Setter fontFamily: string @@ -235,6 +246,10 @@ export default function Editor(props: EditorProps) { fontFamily, snippetBackgroundColor: backgroundColor, backgroundColor: props.bgColor, + backgroundType: props.bgType, + backgroundGradientColorStart: props.bgGradientColorStart, + backgroundGradientColorEnd: props.bgGradientColorEnd, + backgroundGradientDirection: props.bgGradientDirection, }, }, ) @@ -352,22 +367,126 @@ export default function Editor(props: EditorProps) { - - - - props.setBgColor(e.target.value)} - /> - + + + Background + + + + Type + + + props.setBgType('solid')} + > + Solid + + + + + props.setBgType('linearGradient')} + > + Linear Gradient + + + + + + + + + {props.bgType === 'linearGradient' && ( + <> + + + { + props.setBgType('linearGradient') + props.setBgGradientColorStart(e.target.value) + }} + /> + + + + { + props.setBgType('linearGradient') + props.setBgGradientColorEnd(e.target.value) + }} + /> + + + { + props.setBgType('linearGradient') + props.setBgGradientDirection(e[0]) + }} + > +
+ Direction +
+ + + + +
+ + deg +
+
+
+ + )} + {props.bgType === 'solid' && ( + + + { + props.setBgType('solid') + props.setBgColor(e.target.value) + }} + /> + + )} +
+
+
+ Layout @@ -599,7 +718,13 @@ export default function Editor(props: EditorProps) { id="styled-snippet" class="flex flex-row items-center justify-center overflow-hidden" style={{ - background: props.bgColor, + ...(props.bgType === 'linearGradient' + ? { + background: `linear-gradient(${props.bgGradientDirection}deg, ${props.bgGradientColorStart}, ${props.bgGradientColorEnd})`, + } + : { + background: props.bgColor, + }), padding: `${props.yPadding}px ${props.xPadding}px`, }} > @@ -717,6 +842,10 @@ export default function Editor(props: EditorProps) { shadowColor: props.shadowColor, shadowOpacity: props.shadowOpacity, bgColor: props.bgColor, + bgType: props.bgType, + bgGradientColorStart: props.bgGradientColorStart, + bgGradientColorEnd: props.bgGradientColorEnd, + bgGradientDirection: props.bgGradientDirection, language: props.language, theme: props.theme, }) @@ -829,8 +958,6 @@ function htmlDecode(str: string) { return txt.value } -const snippetPadding = 16 - async function createAnimationFrame( elements: MagicMoveElement[], frame: number, @@ -840,28 +967,62 @@ async function createAnimationFrame( ) { const { yPadding, xPadding } = config.layout const { shadowEnabled, shadowOffsetY, shadowBlur, shadowColor, shadowOpacity } = config.shadow - const { fontSize, fontFamily, backgroundColor, snippetBackgroundColor } = config.styling + const { + fontSize, + fontFamily, + backgroundColor, + snippetBackgroundColor, + backgroundType, + backgroundGradientColorStart, + backgroundGradientColorEnd, + backgroundGradientDirection, + } = config.styling const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d', { alpha: false }) + const ctx = canvas.getContext('2d', { alpha: false })! canvas.width = width + xPadding * 2 canvas.height = height + yPadding * 2 - ctx!.fillStyle = backgroundColor - ctx?.fillRect(0, 0, canvas.width, canvas.height) - ctx!.fillStyle = snippetBackgroundColor + if (backgroundType === 'linearGradient') { + // Convert angle to match CSS gradient angle (0deg = to top, 90deg = to right) + const cssAngle = (backgroundGradientDirection + 90) % 360 + const angle = cssAngle * (Math.PI / 180) + // canvas use points x1,y1,x2,y2 instead of degree of angle like in css + // calculate the points based on the angle + const w = canvas.width + const h = canvas.height + const diagonal = Math.sqrt(w * w + h * h) + + const x1 = w / 2 + (Math.cos(angle) * diagonal) / 2 + const y1 = h / 2 + (Math.sin(angle) * diagonal) / 2 + const x2 = w / 2 - (Math.cos(angle) * diagonal) / 2 + const y2 = h / 2 - (Math.sin(angle) * diagonal) / 2 + + const grad = ctx.createLinearGradient(x1, y1, x2, y2) + + grad.addColorStop(0, backgroundGradientColorStart) + grad.addColorStop(1, backgroundGradientColorEnd) + + ctx.fillStyle = grad + ctx.fillRect(0, 0, canvas.width, canvas.height) + } else { + ctx.fillStyle = backgroundColor + ctx.fillRect(0, 0, canvas.width, canvas.height) + } + + ctx.fillStyle = snippetBackgroundColor if (shadowEnabled) { - ctx!.shadowColor = `${shadowColor}${(shadowOpacity * 255).toString(16)}` - ctx!.shadowBlur = shadowBlur - ctx!.shadowOffsetX = 0 - ctx!.shadowOffsetY = shadowOffsetY + ctx.shadowColor = `${shadowColor}${(shadowOpacity * 255).toString(16)}` + ctx.shadowBlur = shadowBlur + ctx.shadowOffsetX = 0 + ctx.shadowOffsetY = shadowOffsetY } - ctx!.beginPath() - ctx!.roundRect(xPadding, yPadding, width, height, 4) - ctx!.fill() + ctx.beginPath() + ctx.roundRect(xPadding, yPadding, width, height, 4) + ctx.fill() - ctx!.shadowColor = 'transparent' + ctx.shadowColor = 'transparent' const xModifier = xPadding const yModifier = yPadding + parseInt(fontSize) @@ -892,12 +1053,12 @@ async function createAnimationFrame( [el.color.start || 'rgba(0,0,0,0)', el.color.end || 'rgba(0,0,0,0)'], ) - ctx!.font = `${fontSize} ${fontFamily}` - ctx!.fillStyle = color - ctx!.globalAlpha = opacity - ctx!.fillText(htmlDecode(el.el.innerHTML), x, y, width - x + xPadding / 2) + ctx.font = `${fontSize} ${fontFamily}` + ctx.fillStyle = color + ctx.globalAlpha = opacity + ctx.fillText(htmlDecode(el.el.innerHTML), x, y, width - x + xPadding / 2) }) await Promise.all(elementPromises) - return ctx!.getImageData(0, 0, canvas.width, canvas.height) + return ctx.getImageData(0, 0, canvas.width, canvas.height) } diff --git a/playgrounds/app/src/db/schema.ts b/playgrounds/app/src/db/schema.ts index cc2c70e..2bc4bb5 100644 --- a/playgrounds/app/src/db/schema.ts +++ b/playgrounds/app/src/db/schema.ts @@ -29,6 +29,10 @@ export const snippetsTable = sqliteTable('snippets_table', { shadowColor: text().notNull().default('#000000'), shadowOpacity: real().notNull().default(0.6), bgColor: text().notNull().default('#ffffff'), + bgType: text().notNull().default('solid'), + bgGradientColorStart: text().default('#ffffff'), + bgGradientColorEnd: text().default('#ffffff'), + bgGradientDirection: int().default(0), language: text().notNull().default('tsx'), theme: text().notNull().default('nord'), fontSize: int().notNull().default(16), diff --git a/playgrounds/app/src/lib/validators.ts b/playgrounds/app/src/lib/validators.ts index c82770b..1b947ba 100644 --- a/playgrounds/app/src/lib/validators.ts +++ b/playgrounds/app/src/lib/validators.ts @@ -18,6 +18,10 @@ export const snippetValidator = z.object({ shadowBlur: z.number().min(1).max(200), shadowColor: z.string().min(1).max(30), shadowOpacity: z.number().min(0).max(1), + bgType: z.enum(['solid', 'linearGradient']), + bgGradientColorStart: z.string().min(1).max(30), + bgGradientColorEnd: z.string().min(1).max(30), + bgGradientDirection: z.number().min(0).max(360), bgColor: z.string().min(1).max(30), language: z.string().min(1).max(64), theme: z.string().min(1).max(64), diff --git a/playgrounds/app/src/routes/api/snippets.ts b/playgrounds/app/src/routes/api/snippets.ts index a7f08ee..bc3bcae 100644 --- a/playgrounds/app/src/routes/api/snippets.ts +++ b/playgrounds/app/src/routes/api/snippets.ts @@ -61,6 +61,10 @@ export async function POST(event: APIEvent) { shadowOpacity, bgColor, language, + bgType, + bgGradientColorStart, + bgGradientColorEnd, + bgGradientDirection, theme, } = requestBody @@ -81,6 +85,10 @@ export async function POST(event: APIEvent) { shadowColor, shadowOpacity, bgColor, + bgType, + bgGradientColorStart, + bgGradientColorEnd, + bgGradientDirection, language, theme, } diff --git a/playgrounds/app/src/routes/api/snippets/[snippetId].ts b/playgrounds/app/src/routes/api/snippets/[snippetId].ts index 8a9064c..c5e2c62 100644 --- a/playgrounds/app/src/routes/api/snippets/[snippetId].ts +++ b/playgrounds/app/src/routes/api/snippets/[snippetId].ts @@ -85,6 +85,10 @@ export async function PUT(event: APIEvent) { bgColor, language, theme, + bgType, + bgGradientColorStart, + bgGradientColorEnd, + bgGradientDirection, } = await event.request.json() const isValid = snippetValidator.safeParse({ title, codeLeft, codeRight }) @@ -117,6 +121,10 @@ export async function PUT(event: APIEvent) { bgColor, language, theme, + bgType, + bgGradientColorStart, + bgGradientColorEnd, + bgGradientDirection, }) .where(eq(snippetsTable.id, snippetId)) diff --git a/playgrounds/app/src/routes/index.tsx b/playgrounds/app/src/routes/index.tsx index 74dc758..6adafd4 100644 --- a/playgrounds/app/src/routes/index.tsx +++ b/playgrounds/app/src/routes/index.tsx @@ -70,6 +70,19 @@ export default function Home() { name: 'fontFamily', }) + const [bgType, setBgType] = makePersisted(createSignal<'solid' | 'linearGradient'>('solid'), { + name: 'bgType', + }) + const [bgGradientColorStart, setBgGradientColorStart] = makePersisted(createSignal('#a3d0ff'), { + name: 'bgGradientColorStart', + }) + const [bgGradientColorEnd, setBgGradientColorEnd] = makePersisted(createSignal('#a3d0ff'), { + name: 'bgGradientColorEnd', + }) + const [bgGradientDirection, setBgGradientDirection] = makePersisted(createSignal(45), { + name: 'bgGradientDirection', + }) + return (

@@ -98,6 +111,14 @@ export default function Home() { setShadowOpacity={setShadowOpacity} bgColor={bgColor()} setBgColor={setBgColor} + bgType={bgType()} + setBgType={setBgType} + bgGradientColorStart={bgGradientColorStart()} + setBgGradientColorStart={setBgGradientColorStart} + bgGradientColorEnd={bgGradientColorEnd()} + setBgGradientColorEnd={setBgGradientColorEnd} + bgGradientDirection={bgGradientDirection()} + setBgGradientDirection={setBgGradientDirection} fontSize={fontSize()} setFontSize={setFontSize} fontFamily={fontFamily()} diff --git a/playgrounds/app/src/routes/snippets/[snippetId].tsx b/playgrounds/app/src/routes/snippets/[snippetId].tsx index 53326a5..8a1833c 100644 --- a/playgrounds/app/src/routes/snippets/[snippetId].tsx +++ b/playgrounds/app/src/routes/snippets/[snippetId].tsx @@ -39,6 +39,14 @@ export default function ViewSnippet({ params }: { params: { snippetId: string } setShadowOpacity={() => {}} bgColor={snippet()!.bgColor} setBgColor={() => {}} + bgType={snippet()!.bgType} + setBgType={() => {}} + bgGradientColorStart={snippet()!.bgGradientColorStart} + setBgGradientColorStart={() => {}} + bgGradientColorEnd={snippet()!.bgGradientColorEnd} + setBgGradientColorEnd={() => {}} + bgGradientDirection={snippet()!.bgGradientDirection} + setBgGradientDirection={() => {}} fontSize={snippet()!.fontSize} setFontSize={() => {}} fontFamily={snippet()!.fontFamily} diff --git a/playgrounds/app/src/types.ts b/playgrounds/app/src/types.ts index fa57377..e674e8c 100644 --- a/playgrounds/app/src/types.ts +++ b/playgrounds/app/src/types.ts @@ -22,6 +22,10 @@ export interface Snippet { shadowBlur: number shadowColor: string shadowOpacity: number + bgType: 'solid' | 'linearGradient' + bgGradientColorStart: string + bgGradientColorEnd: string + bgGradientDirection: number bgColor: string language: string theme: string @@ -47,6 +51,10 @@ export interface AnimationFrameStyling { fontFamily: string snippetBackgroundColor: string backgroundColor: string + backgroundType: string + backgroundGradientColorStart: string + backgroundGradientColorEnd: string + backgroundGradientDirection: number } export interface AnimationFrameConfig {