diff --git a/js-peer/next.config.js b/js-peer/next.config.js index 61152ed5..e7892cf7 100644 --- a/js-peer/next.config.js +++ b/js-peer/next.config.js @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', + trailingSlash: true, reactStrictMode: true, productionBrowserSourceMaps: true, images: { diff --git a/js-peer/src/components/nav.tsx b/js-peer/src/components/nav.tsx index e4e221f1..0347fa25 100644 --- a/js-peer/src/components/nav.tsx +++ b/js-peer/src/components/nav.tsx @@ -5,7 +5,11 @@ import Link from 'next/link' import Image from 'next/image' import { useRouter } from 'next/router' -const navigationItems = [{ name: 'Source', href: 'https://github.com/libp2p/universal-connectivity' }] +const navigationItems = [ + { name: 'Chat', href: '/' }, + { name: 'Pixel Art', href: '/pixel-art' }, + { name: 'Source', href: 'https://github.com/libp2p/universal-connectivity' }, +] function classNames(...classes: string[]) { return classes.filter(Boolean).join(' ') diff --git a/js-peer/src/components/pixel-art-editor.tsx b/js-peer/src/components/pixel-art-editor.tsx new file mode 100644 index 00000000..8f39965e --- /dev/null +++ b/js-peer/src/components/pixel-art-editor.tsx @@ -0,0 +1,351 @@ +import React, { useEffect, useRef, useState } from 'react' +import { usePixelArtContext, GRID_SIZE } from '@/context/pixel-art-ctx' +import { Button } from './button' +import { presets } from '@/lib/pixel-art-presets' + +// Predefined color palette +const colorPalette = [ + '#000000', // Black + '#FFFFFF', // White + '#FF0000', // Red + '#00FF00', // Green + '#0000FF', // Blue + '#FFFF00', // Yellow + '#FF00FF', // Magenta + '#00FFFF', // Cyan + '#FFA500', // Orange + '#800080', // Purple + '#008000', // Dark Green + '#800000', // Maroon + '#008080', // Teal + '#FFC0CB', // Pink + '#A52A2A', // Brown + '#808080', // Gray + '#C0C0C0', // Silver + '#000080', // Navy + '#FFD700', // Gold + '#4B0082', // Indigo +] + +export default function PixelArtEditor() { + const { + pixelArtState, + setPixel, + selectedColor, + setSelectedColor, + clearCanvas, + loadPreset, + requestFullState, + broadcastFullState, + } = usePixelArtContext() + const canvasRef = useRef(null) + const [isDrawing, setIsDrawing] = useState(false) + const [canvasSize, setCanvasSize] = useState(512) // Default canvas size + const [showPresets, setShowPresets] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) + const [pixelCount, setPixelCount] = useState(0) + const [debugMode, setDebugMode] = useState(false) + const [showGrid, setShowGrid] = useState(true) + const pixelSize = canvasSize / GRID_SIZE + + // Function to draw the grid and pixels + const drawCanvas = () => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext('2d') + if (!ctx) return + + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Draw the background (white) + ctx.fillStyle = '#FFFFFF' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Draw the grid lines if enabled + if (showGrid) { + ctx.strokeStyle = '#EEEEEE' + ctx.lineWidth = 1 + + // Draw vertical grid lines + for (let x = 0; x <= GRID_SIZE; x++) { + ctx.beginPath() + ctx.moveTo(x * pixelSize, 0) + ctx.lineTo(x * pixelSize, canvas.height) + ctx.stroke() + } + + // Draw horizontal grid lines + for (let y = 0; y <= GRID_SIZE; y++) { + ctx.beginPath() + ctx.moveTo(0, y * pixelSize) + ctx.lineTo(canvas.width, y * pixelSize) + ctx.stroke() + } + } + + // Draw the pixels + pixelArtState.grid.forEach((pixel) => { + ctx.fillStyle = pixel.color + ctx.fillRect(pixel.x * pixelSize, pixel.y * pixelSize, pixelSize, pixelSize) + }) + } + + // Handle canvas resize + useEffect(() => { + const handleResize = () => { + // Adjust canvas size based on window width + const containerWidth = Math.min(window.innerWidth - 40, 512) + setCanvasSize(containerWidth) + } + + handleResize() + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + // Draw the canvas whenever the pixel art state changes or canvas size changes + useEffect(() => { + drawCanvas() + setPixelCount(pixelArtState.grid.length) + }, [pixelArtState, canvasSize, showGrid]) + + // Convert mouse/touch position to grid coordinates + const getGridCoordinates = (clientX: number, clientY: number) => { + const canvas = canvasRef.current + if (!canvas) return { x: -1, y: -1 } + + const rect = canvas.getBoundingClientRect() + const x = Math.floor((clientX - rect.left) / pixelSize) + const y = Math.floor((clientY - rect.top) / pixelSize) + + // Ensure coordinates are within grid bounds + if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { + return { x, y } + } + + return { x: -1, y: -1 } + } + + // Mouse/touch event handlers + const handleMouseDown = (e: React.MouseEvent) => { + setIsDrawing(true) + const { x, y } = getGridCoordinates(e.clientX, e.clientY) + if (x >= 0 && y >= 0) { + setPixel(x, y, selectedColor) + } + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDrawing) return + + const { x, y } = getGridCoordinates(e.clientX, e.clientY) + if (x >= 0 && y >= 0) { + setPixel(x, y, selectedColor) + } + } + + const handleMouseUp = () => { + setIsDrawing(false) + } + + const handleMouseLeave = () => { + setIsDrawing(false) + } + + // Touch event handlers + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault() + setIsDrawing(true) + + const touch = e.touches[0] + const { x, y } = getGridCoordinates(touch.clientX, touch.clientY) + if (x >= 0 && y >= 0) { + setPixel(x, y, selectedColor) + } + } + + const handleTouchMove = (e: React.TouchEvent) => { + e.preventDefault() + if (!isDrawing) return + + const touch = e.touches[0] + const { x, y } = getGridCoordinates(touch.clientX, touch.clientY) + if (x >= 0 && y >= 0) { + setPixel(x, y, selectedColor) + } + } + + const handleTouchEnd = () => { + setIsDrawing(false) + } + + // Handle loading a preset + const handleLoadPreset = (presetName: string) => { + const presetFunction = presets[presetName as keyof typeof presets] + if (presetFunction) { + loadPreset(presetFunction()) + setShowPresets(false) // Hide presets after selection + } + } + + // Handle refreshing the canvas + const handleRefreshCanvas = () => { + setIsRefreshing(true) + requestFullState() + + // Reset the refreshing state after a timeout + setTimeout(() => { + setIsRefreshing(false) + }, 2000) + } + + // Handle broadcasting the full state + const handleBroadcastState = () => { + if (pixelArtState.grid.length > 0) { + broadcastFullState() + } + } + + // Toggle grid visibility + const toggleGrid = () => { + setShowGrid(!showGrid) + } + + return ( +
+

Collaborative Pixel Art

+

Draw together with peers in real-time! 🎨

+ +
+ +
+
32 x 32 grid
+ +
+
+ +
+
+ {colorPalette.map((color) => ( +
+ +
+ + + + + +
+ + {showPresets && ( +
+

Preset Art

+
+ {Object.keys(presets).map((presetName) => ( + + ))} +
+
+ )} +
+ +
+

Connected peers will see your artwork in real-time!

+

New peers will automatically receive the current canvas state.

+

+ Current pixel count: {pixelCount} +

+ +
+ setDebugMode(!debugMode)} + className="mr-2" + /> + +
+ + {debugMode && ( +
+

Pixel Data (most recent 5):

+
    + {pixelArtState.grid + .sort((a, b) => b.timestamp - a.timestamp) + .slice(0, 5) + .map((pixel, index) => ( +
  • + ({pixel.x}, {pixel.y}) - {pixel.color} - {new Date(pixel.timestamp).toLocaleTimeString()} -{' '} + {pixel.peerId.substring(0, 8)}... +
  • + ))} +
+
+ +
+
+ )} +
+
+ ) +} diff --git a/js-peer/src/context/ctx.tsx b/js-peer/src/context/ctx.tsx index 56743506..15e17c0f 100644 --- a/js-peer/src/context/ctx.tsx +++ b/js-peer/src/context/ctx.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react' import { startLibp2p } from '../lib/libp2p' import { ChatProvider } from './chat-ctx' +import { PixelArtProvider } from './pixel-art-ctx' import type { Libp2p, PubSub } from '@libp2p/interface' import type { Identify } from '@libp2p/identify' import type { DirectMessage } from '@/lib/direct-message' @@ -58,7 +59,9 @@ export function AppWrapper({ children }: WrapperProps) { return ( - {children} + + {children} + ) } diff --git a/js-peer/src/context/pixel-art-ctx.tsx b/js-peer/src/context/pixel-art-ctx.tsx new file mode 100644 index 00000000..368a940f --- /dev/null +++ b/js-peer/src/context/pixel-art-ctx.tsx @@ -0,0 +1,325 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' +import { useLibp2pContext } from './ctx' +import type { Message } from '@libp2p/interface' +import { PIXEL_ART_TOPIC } from '@/lib/constants' +import { forComponent } from '@/lib/logger' + +const log = forComponent('pixel-art-context') + +// Define the pixel grid size +export const GRID_SIZE = 32 + +// Define the pixel art data structure +export interface PixelData { + x: number + y: number + color: string + timestamp: number + peerId: string +} + +export interface PixelArtState { + grid: PixelData[] +} + +export interface PixelArtContextInterface { + pixelArtState: PixelArtState + setPixel: (x: number, y: number, color: string) => void + selectedColor: string + setSelectedColor: (color: string) => void + clearCanvas: () => void + loadPreset: (pixels: PixelData[]) => void + requestFullState: () => void + broadcastFullState: () => void +} + +export const pixelArtContext = createContext({ + pixelArtState: { grid: [] }, + setPixel: () => {}, + selectedColor: '#000000', + setSelectedColor: () => {}, + clearCanvas: () => {}, + loadPreset: () => {}, + requestFullState: () => {}, + broadcastFullState: () => {}, +}) + +export const usePixelArtContext = () => { + return useContext(pixelArtContext) +} + +export const PixelArtProvider = ({ children }: { children: React.ReactNode }) => { + const [pixelArtState, setPixelArtState] = useState({ grid: [] }) + const [selectedColor, setSelectedColor] = useState('#000000') + const { libp2p } = useLibp2pContext() + + // Function to update a pixel and broadcast the change + const setPixel = (x: number, y: number, color: string) => { + if (!libp2p) return + + const pixelData: PixelData = { + x, + y, + color, + timestamp: Date.now(), + peerId: libp2p.peerId.toString(), + } + + // Update local state + updatePixelState(pixelData) + + // Broadcast the change to other peers + const pixelDataString = JSON.stringify(pixelData) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(pixelDataString)) + } + + // Function to clear the canvas and broadcast the change + const clearCanvas = () => { + if (!libp2p) return + + // Update local state + setPixelArtState({ grid: [] }) + + // Broadcast the clear action to other peers + const clearAction = JSON.stringify({ action: 'clear', timestamp: Date.now(), peerId: libp2p.peerId.toString() }) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(clearAction)) + } + + // Function to load a preset and broadcast the change + const loadPreset = (pixels: PixelData[]) => { + if (!libp2p) return + + // Clear the canvas first + clearCanvas() + + // Update local state with all preset pixels + setPixelArtState({ grid: pixels }) + + // Broadcast each pixel to other peers + pixels.forEach((pixel) => { + const pixelDataString = JSON.stringify({ + ...pixel, + peerId: libp2p.peerId.toString(), // Use the current peer ID for broadcasting + }) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(pixelDataString)) + }) + } + + // Function to request the full state from other peers + const requestFullState = () => { + if (!libp2p) return + + const requestMessage = JSON.stringify({ + action: 'requestState', + timestamp: Date.now(), + peerId: libp2p.peerId.toString(), + }) + + log(`Requesting full pixel art state from peers`) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(requestMessage)) + } + + // Function to send the full state to a requesting peer + const sendFullState = (requestingPeerId: string) => { + if (!libp2p) return + + // Even if we have no pixels, send an empty state to prevent repeated requests + const fullStateMessage = JSON.stringify({ + action: 'fullState', + timestamp: Date.now(), + peerId: libp2p.peerId.toString(), + targetPeerId: requestingPeerId, + state: pixelArtState, + }) + + log(`Sending full pixel art state to peer ${requestingPeerId}`) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(fullStateMessage)) + } + + // Function to broadcast the full state to all peers + const broadcastFullState = () => { + if (!libp2p) return + + const fullStateMessage = JSON.stringify({ + action: 'fullState', + timestamp: Date.now(), + peerId: libp2p.peerId.toString(), + // No targetPeerId means broadcast to all + state: pixelArtState, + }) + + log(`Broadcasting full pixel art state to all peers (${pixelArtState.grid.length} pixels)`) + libp2p.services.pubsub.publish(PIXEL_ART_TOPIC, new TextEncoder().encode(fullStateMessage)) + } + + // Function to update the pixel state + const updatePixelState = (pixelData: PixelData) => { + setPixelArtState((prevState) => { + // Create a copy of the current grid + const updatedGrid = [...prevState.grid] + + // Find if the pixel already exists + const existingPixelIndex = updatedGrid.findIndex((pixel) => pixel.x === pixelData.x && pixel.y === pixelData.y) + + if (existingPixelIndex !== -1) { + // Update existing pixel if the new one is more recent + if (pixelData.timestamp > updatedGrid[existingPixelIndex].timestamp) { + updatedGrid[existingPixelIndex] = pixelData + } + } else { + // Add new pixel + updatedGrid.push(pixelData) + } + + return { grid: updatedGrid } + }) + } + + // Handle incoming pixel art messages + const handlePixelArtMessage = (evt: CustomEvent) => { + if (evt.detail.topic !== PIXEL_ART_TOPIC || evt.detail.type !== 'signed') { + return + } + + try { + const data = new TextDecoder().decode(evt.detail.data) + const parsedData = JSON.parse(data) + const senderPeerId = evt.detail.from.toString() + + // Handle clear action + if (parsedData.action === 'clear') { + log(`Received clear canvas action from ${senderPeerId}`) + setPixelArtState({ grid: [] }) + return + } + + // Handle state request + if (parsedData.action === 'requestState') { + const requestingPeerId = parsedData.peerId + if (requestingPeerId !== libp2p.peerId.toString()) { + log(`Received state request from peer ${requestingPeerId}`) + + // Add a small random delay to prevent all peers from responding at once + setTimeout(() => { + sendFullState(requestingPeerId) + }, Math.random() * 1000) + } + return + } + + // Handle full state + if (parsedData.action === 'fullState') { + // Only process if we're the target or if it's a broadcast to all + if (parsedData.targetPeerId === libp2p.peerId.toString() || !parsedData.targetPeerId) { + log(`Received full state from peer ${senderPeerId} with ${parsedData.state.grid.length} pixels`) + + // Only update if the received state has pixels and either: + // 1. Our canvas is empty, or + // 2. The received state is newer based on the most recent pixel timestamp + if ( + parsedData.state.grid.length > 0 && + (pixelArtState.grid.length === 0 || + (parsedData.state.grid.length > 0 && + Math.max(...parsedData.state.grid.map((p: PixelData) => p.timestamp)) > + Math.max(...pixelArtState.grid.map((p) => p.timestamp || 0)))) + ) { + log(`Updating canvas with received state (${parsedData.state.grid.length} pixels)`) + setPixelArtState(parsedData.state) + } else { + log(`Ignoring received state (${parsedData.state.grid.length} pixels) as our state is newer or equal`) + } + } + return + } + + // Handle pixel update + const pixelData = parsedData as PixelData + log( + `Received pixel update at (${pixelData.x}, ${pixelData.y}) with color ${pixelData.color} from ${senderPeerId}`, + ) + updatePixelState(pixelData) + } catch (error) { + console.error('Error parsing pixel art message:', error) + } + } + + // Subscribe to the pixel art topic when the component mounts + useEffect(() => { + if (!libp2p) return + + // Subscribe to the pixel art topic + libp2p.services.pubsub.subscribe(PIXEL_ART_TOPIC) + + // Add event listener for incoming messages + libp2p.services.pubsub.addEventListener('message', handlePixelArtMessage) + + // Wait a moment before requesting the current state to ensure subscription is active + const timer = setTimeout(() => { + requestFullState() + }, 1000) + + return () => { + // Cleanup when the component unmounts + clearTimeout(timer) + libp2p.services.pubsub.removeEventListener('message', handlePixelArtMessage) + libp2p.services.pubsub.unsubscribe(PIXEL_ART_TOPIC) + } + }, [libp2p]) + + // Listen for peer discovery events to request state from new peers + useEffect(() => { + if (!libp2p) return + + const handlePeerDiscovery = (event: any) => { + const peerId = event.detail.id.toString() + log(`Discovered peer: ${peerId}`) + + // Only request state if we don't have any pixels yet + if (pixelArtState.grid.length === 0) { + // Add a small delay to ensure the peer has time to set up + setTimeout(() => { + requestFullState() + }, 2000) + } + } + + libp2p.addEventListener('peer:discovery', handlePeerDiscovery) + + return () => { + libp2p.removeEventListener('peer:discovery', handlePeerDiscovery) + } + }, [libp2p, pixelArtState.grid.length]) + + // Periodically broadcast the full state to ensure all peers are in sync + useEffect(() => { + if (!libp2p) return + + // Only broadcast if we have pixels to share + if (pixelArtState.grid.length === 0) return + + const interval = setInterval(() => { + broadcastFullState() + }, 30000) // Every 30 seconds + + return () => { + clearInterval(interval) + } + }, [libp2p, pixelArtState.grid.length]) + + return ( + + {children} + + ) +} diff --git a/js-peer/src/lib/constants.ts b/js-peer/src/lib/constants.ts index dbbcdd43..ea0bc3ae 100644 --- a/js-peer/src/lib/constants.ts +++ b/js-peer/src/lib/constants.ts @@ -3,6 +3,7 @@ export const CHAT_FILE_TOPIC = 'universal-connectivity-file' export const PUBSUB_PEER_DISCOVERY = 'universal-connectivity-browser-peer-discovery' export const FILE_EXCHANGE_PROTOCOL = '/universal-connectivity-file/1' export const DIRECT_MESSAGE_PROTOCOL = '/universal-connectivity/dm/1.0.0' +export const PIXEL_ART_TOPIC = 'universal-connectivity-pixel-art' export const CIRCUIT_RELAY_CODE = 290 diff --git a/js-peer/src/lib/pixel-art-presets.ts b/js-peer/src/lib/pixel-art-presets.ts new file mode 100644 index 00000000..e7251494 --- /dev/null +++ b/js-peer/src/lib/pixel-art-presets.ts @@ -0,0 +1,314 @@ +import { PixelData } from '@/context/pixel-art-ctx' + +// Helper function to create pixel data from a pattern +const createPixelArt = ( + pattern: string[], + colorMap: Record, + offsetX: number = 0, + offsetY: number = 0, +): PixelData[] => { + const pixels: PixelData[] = [] + const timestamp = Date.now() + + pattern.forEach((row, y) => { + row.split('').forEach((char, x) => { + if (char !== ' ' && colorMap[char]) { + pixels.push({ + x: x + offsetX, + y: y + offsetY, + color: colorMap[char], + timestamp, + peerId: 'preset', // Mark as preset + }) + } + }) + }) + + return pixels +} + +// Space Invader 1 - Classic +export const spaceInvader1 = () => { + const pattern = [' ██ ', ' ██ ', ' ████ ', ' ██ ██ ', ' █████ ', ' █ █ ', ' █ █ '] + + const colorMap: Record = { + '█': '#00FF00', // Green + } + + return createPixelArt(pattern, colorMap, 12, 12) +} + +// Space Invader 2 - Detailed +export const spaceInvader2 = () => { + const pattern = [ + ' █ █ ', + ' █ █ ', + ' ██████ ', + ' ████████ ', + '██ ████ ██', + '██████████', + '█ ██ ██ █', + ' █ █ ', + ] + + const colorMap: Record = { + '█': '#FF00FF', // Magenta + } + + return createPixelArt(pattern, colorMap, 11, 12) +} + +// Pac-Man - Improved +export const pacMan = () => { + const pattern = [ + ' ●●●● ', + ' ●●●●●● ', + '●●●●●● ', + '●●●● ', + '●●● ', + '●●●● ', + '●●●●●● ', + ' ●●●●●● ', + ' ●●●● ', + ] + + const colorMap: Record = { + '●': '#FFFF00', // Yellow + } + + return createPixelArt(pattern, colorMap, 12, 11) +} + +// Ghost - Blinky (Red) +export const ghost = () => { + const pattern = [ + ' ██████ ', + ' ████████ ', + '██████████', + '██████████', + '██████████', + '██████████', + '██████████', + '██ ██ ██ ', + ' ██ ██ ', + ] + + const colorMap: Record = { + '█': '#FF0000', // Red + } + + return createPixelArt(pattern, colorMap, 11, 11) +} + +// Ghost - Inky (Blue) +export const blueGhost = () => { + const pattern = [ + ' ██████ ', + ' ████████ ', + '██████████', + '██████████', + '██████████', + '██████████', + '██████████', + '██ ██ ██ ', + ' ██ ██ ', + ] + + const colorMap: Record = { + '█': '#00FFFF', // Cyan + } + + return createPixelArt(pattern, colorMap, 11, 11) +} + +// Cherry +export const cherry = () => { + const pattern = [ + ' ██ ', + ' ████ ', + ' ██ ', + ' █ ', + ' ██ ', + ' ███ ', + ' ████ ', + ' ████ ', + ' ███ ', + ' ██ ', + ' █ ', + ' █ ', + ' ███ ', + ' █████ ', + ' ███████', + ' ███████', + ' █████ ', + ] + + const colorMap: Record = { + '█': '#FF0000', // Red + } + + return createPixelArt(pattern, colorMap, 11, 7) +} + +// Mario +export const mario = () => { + const pattern = [ + ' rrrrr ', + ' rrrrrrr ', + ' bbsssb ', + 'bssbsssb ', + 'bssbssbb ', + 'bbbssssb ', + ' sssss ', + ' rrbrrb ', + 'rrrbbbrrr', + 'bbbbbbbbb', + 'bbbbbbbb ', + ' bb bb ', + 'ooo ooo ', + ] + + const colorMap: Record = { + r: '#FF0000', // Red + b: '#A52A2A', // Brown + s: '#FFC0CB', // Skin/Pink + o: '#FFA500', // Orange + } + + return createPixelArt(pattern, colorMap, 11, 9) +} + +// Mushroom +export const mushroom = () => { + const pattern = [ + ' wwww ', + ' wwrrrrww ', + 'wrrwwwwrrw', + 'wrwwwwwwrw', + 'wrwwwwwwrw', + 'wrrrrrrrrw', + 'wrrrrrrrrw', + ' wwwsswww ', + ' ssss ', + ' sssss ', + ' sssss ', + ] + + const colorMap: Record = { + r: '#FF0000', // Red + w: '#FFFFFF', // White + s: '#FFC0CB', // Skin/Pink + } + + return createPixelArt(pattern, colorMap, 11, 10) +} + +// Heart +export const heart = () => { + const pattern = [' rr rr ', 'rrrr rrrr', 'rrrrrrrr ', 'rrrrrrrr ', ' rrrrrr ', ' rrrr ', ' rr '] + + const colorMap: Record = { + r: '#FF0000', // Red + } + + return createPixelArt(pattern, colorMap, 11, 12) +} + +// Star +export const star = () => { + const pattern = [ + ' y ', + ' y ', + ' yyy ', + 'yyyyyyyy ', + ' yyyyyy ', + ' yyyy ', + ' yy yy ', + 'y y ', + ] + + const colorMap: Record = { + y: '#FFFF00', // Yellow + } + + return createPixelArt(pattern, colorMap, 11, 12) +} + +// Link (from Zelda) +export const link = () => { + const pattern = [ + ' ggggg ', + ' ggggggg ', + ' gsssssg ', + 'sssssssss', + 'sssssssss', + 'sssbsbsss', + ' sssssss ', + ' gbbg ', + ' gggbggg ', + 'ggggbgggg', + 'ggggbgggg', + ' ggbbbgg ', + ' ggggg ', + ' bb bb ', + 'bbbb bbbb', + ] + + const colorMap: Record = { + g: '#008000', // Green + s: '#FFC0CB', // Skin/Pink + b: '#A52A2A', // Brown + } + + return createPixelArt(pattern, colorMap, 11, 8) +} + +// Creeper (Minecraft) +export const creeper = () => { + const pattern = ['gggggggg', 'gggggggg', 'gg gg', 'gg gg', 'g g', 'g gg g', 'g gggg g', 'gggggggg'] + + const colorMap: Record = { + g: '#008000', // Green + } + + return createPixelArt(pattern, colorMap, 12, 12) +} + +// Sword +export const sword = () => { + const pattern = [ + ' g ', + ' g ', + ' g ', + ' g ', + ' g ', + ' g ', + ' ggg ', + ' bbb ', + ' b ', + ] + + const colorMap: Record = { + g: '#C0C0C0', // Silver + b: '#A52A2A', // Brown + } + + return createPixelArt(pattern, colorMap, 11, 11) +} + +// Export all presets +export const presets = { + 'Space Invader 1': spaceInvader1, + 'Space Invader 2': spaceInvader2, + 'Pac-Man': pacMan, + 'Red Ghost': ghost, + 'Blue Ghost': blueGhost, + Cherry: cherry, + Mario: mario, + Mushroom: mushroom, + Heart: heart, + Star: star, + Link: link, + Creeper: creeper, + Sword: sword, +} diff --git a/js-peer/src/pages/pixel-art.tsx b/js-peer/src/pages/pixel-art.tsx new file mode 100644 index 00000000..35b3b55c --- /dev/null +++ b/js-peer/src/pages/pixel-art.tsx @@ -0,0 +1,34 @@ +import Head from 'next/head' +import Nav from '@/components/nav' +import PixelArtEditor from '@/components/pixel-art-editor' +import ConnectionPanel from '@/components/connection-panel' +import { useState } from 'react' +import ConnectionInfoButton from '@/components/connection-info-button' + +export default function PixelArt() { + const [isConnectionPanelOpen, setIsConnectionPanelOpen] = useState(false) + + const handleOpenConnectionPanel = () => { + setIsConnectionPanelOpen(true) + } + + return ( + <> + + Pixel Art - Universal Connectivity + + + + +
+
+ setIsConnectionPanelOpen(false)} /> + + ) +}