(feat): make text appear on screen and minimap

This commit is contained in:
Triston Armstrong 2024-10-06 12:42:36 -04:00 committed by Triston Armstrong
parent a7522db83d
commit c364996229
Signed by: tristonarmstrong
GPG Key ID: A23B48AE45EB6EFE
8 changed files with 237 additions and 3 deletions

View File

@ -1,5 +1,5 @@
import { useRef, useEffect } from "kaioken" import { useRef, useEffect } from "kaioken"
import { ImagesSignal, NotesSigal, canvasDimentsion } from "../signals" import { ImagesSignal, NotesSigal, TextSignal, canvasDimentsion } from "../signals"
import { NoteCard } from "./NoteCard" import { NoteCard } from "./NoteCard"
import notes from "../signals/notes" import notes from "../signals/notes"
import { MiniMap } from "./MiniMap" import { MiniMap } from "./MiniMap"
@ -8,6 +8,8 @@ import images from "../signals/images"
import { CardSelector } from "./cardSelector/CardSelector" import { CardSelector } from "./cardSelector/CardSelector"
import { Logo } from "./Logo" import { Logo } from "./Logo"
import { useThemeDetector } from "../utils/useThemeDetector" import { useThemeDetector } from "../utils/useThemeDetector"
import { isTheme } from "../utils/isTheme"
import { TextItem } from "./TextItem"
export default function InfiniteCanvas() { export default function InfiniteCanvas() {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@ -74,6 +76,14 @@ export default function InfiniteCanvas() {
<ImageCard key={itemKey} data={item} /> <ImageCard key={itemKey} data={item} />
) )
})} })}
{Object.keys(TextSignal.default.texts.value).map((itemKey: string) => {
const item = TextSignal.default.texts.value[itemKey]
return (
<TextItem key={itemKey} data={item} />
)
})}
</div> </div>
</div> </div>
</> </>

View File

@ -3,6 +3,7 @@ import notes, { NoteCardType } from "../signals/notes"
import { canvasDimentsion } from "../signals" import { canvasDimentsion } from "../signals"
import { LayerEnum } from "../utils/enums" import { LayerEnum } from "../utils/enums"
import images, { ImageCardType } from "../signals/images" import images, { ImageCardType } from "../signals/images"
import texts from "../signals/texts"
const _MAP_OFFSET = 20 const _MAP_OFFSET = 20
const _MAP_SCALE_FACTOR = 10 const _MAP_SCALE_FACTOR = 10
@ -111,6 +112,35 @@ export function MiniMap() {
) )
})} })}
{Object.keys(texts.texts.value).map((textKey: textCardType['id']) => {
const text = texts.texts.value[textKey]
const el = useRef(null)
function _handleItemClick(_e: MouseEvent) {
window.scrollTo({
left: text.position.x - ((viewportWidth / 2) - (text.dimensions.w / 2)),
top: text.position.y - ((viewportHeight / 2) - (text.dimensions.h / 2)),
behavior: 'smooth'
})
}
return (
<div ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer"}
onclick={_handleItemClick}
style={{
position: 'absolute',
width: `${text.dimensions.w / _MAP_SCALE_FACTOR}px`,
height: `${text.dimensions.h / _MAP_SCALE_FACTOR}px`,
top: `${(text.position.y / _MAP_SCALE_FACTOR)}px`,
left: `${(text.position.x / _MAP_SCALE_FACTOR)}px`,
border: '1px solid #222',
borderRadius: '2px',
zIndex: `${LayerEnum.MINIMAP + 1}`
}}
></div>
)
})}
<div <div
className={'pointer-events-none absolute bg-blue-200 bg-opacity-10 border dark:border-blue-800 border-blue-500 bg-blue-500 rounded'} className={'pointer-events-none absolute bg-blue-200 bg-opacity-10 border dark:border-blue-800 border-blue-500 bg-blue-500 rounded'}
style={{ style={{

108
src/components/TextItem.tsx Normal file
View File

@ -0,0 +1,108 @@
import { signal, useRef } from "kaioken"
import { TextSignal, focusedItem } from "../signals"
import { useDebounce } from "../utils/useDebounce"
import texts, { TextCardType } from "../signals/texts"
import { LayerEnum } from "../utils/enums"
namespace TextItem {
export interface TextCardProps {
key: TextCardType['id']
data: TextCardType
}
}
export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
const saved = signal(true)
const pressed = signal(false)
const newX = useRef(0)
const newY = useRef(0)
const offsetX = useRef(0)
const offsetY = useRef(0)
const initialResizeX = useRef(0)
const initialResizeY = useRef(0)
const { debounce } = useDebounce()
function updateLocalStorage(time?: number) {
debounce(() => {
localStorage.setItem("texts", JSON.stringify(texts.texts.value))
}, time)
}
function _handleMouseMove(e: MouseEvent) {
e.preventDefault()
if (!pressed.value) return
newX.current = e.pageX - offsetX.current
newY.current = e.pageY - offsetY.current
const newPos = { x: newX.current, y: newY.current }
TextSignal.default.updateTextProperty(itemKey, 'position', newPos)
updateLocalStorage()
}
function _handleMouseUp(e: MouseEvent) {
e.preventDefault()
pressed.value = false
window.removeEventListener('mousemove', _handleMouseMove)
window.removeEventListener('mouseup', _handleMouseUp)
}
function _handleMouseDown(e: MouseEvent) {
e.preventDefault()
offsetX.current = e.offsetX
offsetY.current = e.offsetY
pressed.value = true
window.addEventListener('mousemove', _handleMouseMove)
window.addEventListener('mouseup', _handleMouseUp)
}
function _handleResizeMove(e: MouseEvent) {
const { pageX, pageY } = e
const [newX, newY] = [initialResizeX.current - pageX, initialResizeY.current - pageY]
const newW = -newX + item.dimensions.w
const newH = -newY + item.dimensions.h
const newDim = { w: newW, h: newH }
TextSignal.default.updateTextProperty(itemKey, 'dimensions', newDim)
TextSignal.default.texts.notify()
}
function _handleResizeMouseDown(e: MouseEvent) {
initialResizeX.current = e.pageX
initialResizeY.current = e.pageY
pressed.value = true
window.addEventListener('mousemove', _handleResizeMove)
window.addEventListener('mouseup', _handleResizeMouseUp)
}
function _handleResizeMouseUp() {
pressed.value = false
updateLocalStorage()
window.removeEventListener('mousemove', _handleResizeMove)
window.removeEventListener('mouseup', _handleResizeMouseUp)
}
return (
<div
onmousedown={() => focusedItem.value = itemKey}
className="select-none transition flex flex-col justify-stretch shadow-lg rounded border border-[#3c3c3c] absolute border-dashed"
style={{
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
width: `${item.dimensions.w}px`,
height: `${item.dimensions.h}px`,
top: `${item.position.y}px`,
left: `${item.position.x}px`,
}}
>
<svg viewBox="0 0 56 18">
<text x="0" y="15" fill={'white'}>{item.contents}</text>
</svg>
</div >
)
}

View File

@ -1,8 +1,28 @@
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { TextSignal } from "../../signals";
import texts from "../../signals/texts";
import { updateLocalStorage } from "../../utils/localStorage";
import { defaultClassName } from "./utils"; import { defaultClassName } from "./utils";
export function TextButton() { export function TextButton() {
function _handleClick(e: MouseEvent) {
TextSignal.default.addText({
type: "text",
title: "New Note",
contents: "todo: fill me",
position: {
x: e.pageX - 100,
y: e.pageY + (window.innerHeight / 2) - 100
},
dimensions: {
w: 200,
h: 100
}
})
updateLocalStorage("text", texts.texts.value)
}
return ( return (
<Tooltip message="Create a Text Node"> <Tooltip message="Create a Text Node">
<svg <svg
@ -22,5 +42,22 @@ export function TextButton() {
<path d="M8 12h8" /> <path d="M8 12h8" />
</svg> </svg>
</Tooltip> </Tooltip>
<svg
onclick={_handleClick}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className={defaultClassName}
>
<path d="M4 20h16" />
<path d="m6 16 6-12 6 12" />
<path d="M8 12h8" />
</svg>
) )
} }

View File

@ -6,3 +6,4 @@ export const canvasDimentsion = signal({ width: 3000, height: 3000 })
export * as NotesSigal from "./notes" export * as NotesSigal from "./notes"
export * as ImagesSignal from "./images" export * as ImagesSignal from "./images"
export * as TextSignal from "./texts"

46
src/signals/texts.ts Normal file
View File

@ -0,0 +1,46 @@
import { signal } from "kaioken"
import { Card } from "../types"
import { focusedItem } from "."
export type TextCardType = Card<"text">
const texts = signal<Record<TextCardType["id"], TextCardType>>({})
function loadLocalStorage() {
texts.value = JSON.parse(localStorage.getItem("texts") ?? "{}")
}
function addText(data: Omit<TextCardType, "id">) {
const newCard = {
...data,
id: crypto.randomUUID(),
}
texts.value[newCard.id] = newCard
texts.notify()
focusedItem.value = newCard.id
}
function removeText(id: TextCardType["id"]) {
delete texts.value[id]
texts.notify()
}
function updateTextProperty<K extends keyof TextCardType>(
id: TextCardType["id"],
property: K,
data: TextCardType[K]
) {
const newData = {
...texts.value[id],
[property]: data,
}
texts.value[id] = newData
texts.notify()
}
export default {
texts,
addText,
removeText,
updateTextProperty,
loadLocalStorage,
}

View File

@ -1,4 +1,4 @@
export type CardTypes = "note" | "image" export type CardTypes = "note" | "image" | "text"
export type positionCoords = { x: number; y: number } export type positionCoords = { x: number; y: number }
export type dimensionCoords = { w: number; h: number } export type dimensionCoords = { w: number; h: number }

View File

@ -1,5 +1,7 @@
import { CardTypes } from "../types"
export function updateLocalStorage( export function updateLocalStorage(
location: "notes" | "images", location: CardTypes,
collection: unknown[] | Record<string, unknown> collection: unknown[] | Record<string, unknown>
) { ) {
localStorage.setItem(location, JSON.stringify(collection)) localStorage.setItem(location, JSON.stringify(collection))