Initial diophantine sphere component

This commit is contained in:
2026-06-10 11:41:49 +02:00
commit 6ad4f51838
18 changed files with 8836 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
.DS_Store
+170
View File
@@ -0,0 +1,170 @@
# Diophantine Sphere Component Conversation Log
Created: June 7, 2026
Current folder before relocation:
`/Users/jenstandstad/Documents/glitch_diophantine_sphere`
## Initial Setup
The project began as an empty git folder. The sibling project `../glitch-bubble-simulator` was inspected and used as the structural template. Its standalone Glitch component shape was copied:
- `src/Component.tsx`
- `src/index.tsx`
- `src/dev.tsx`
- `src/styles.module.css`
- `src/types.ts`
- `vite.config.ts`
- `tsconfig.json`
- `package.json`
- `glitch.manifest.json`
- `README.md`
No sibling `SKILL.md` file was found. The readable sibling docs were the README files for `glitch-bubble-simulator` and `glitch_demographic_wave`.
## Core Visualization
The component was renamed and rebuilt as `diophantine-sphere`.
The initial mathematical model:
`|x| + |y| + |z| = C`
where `x`, `y`, and `z` are integers.
Each integer vector is reduced by `gcd(|x|, |y|, |z|)` so collinear integer vectors share one distinct ray. Points are then placed along those rays.
The component uses Three.js for the 3D scene.
## Controls Added
The first controls were:
- allow negative values
- radius
- complexity
Then these were extended into the current control set:
- `Surface`, `Single C`, `Range 1..C` mode switch
- `Allow negative values`
- `Normalize distance`
- `D` for distance / radius
- `C` for complexity
- `R` for dot radius
The public parameter labels in `src/index.tsx` were simplified to `D`, `C`, and `R`.
## Modes
### Surface
Shows a continuous surface, representing infinitely many points.
When `Normalize distance` is checked:
`x² + y² + z² = r²`
This is the round Euclidean sphere.
When `Normalize distance` is unchecked:
`|x| + |y| + |z| = r`
This is the continuous L1 surface, rendered as an octahedral surface.
### Single C
Shows only vectors satisfying:
`|x| + |y| + |z| = C`
This mode is intended to reveal the triangular grid pattern.
### Range 1..C
Shows all complexities from `1` through `C`.
Overlapping rays are merged. Point color indicates how many vectors collapse to the same ray direction.
## Distance Normalization
When `Normalize distance` is checked, ray points are placed on the Euclidean sphere:
`sqrt(x² + y² + z²) = D`
When unchecked, ray points are scaled until:
`|x| + |y| + |z| = D`
This lets the user compare the round Euclidean projection with the taxicab / complexity boundary.
## Color and Style
The visual style was shifted toward a green/yellow retro-futuristic palette.
The overlap color ramp now runs:
green -> yellow -> amber
The layout was simplified:
- explanatory footer hidden
- small kicker labels hidden
- controls are on the left
- square 3D viewport is on the right
- count boxes were moved out of the header flow so they do not shrink the viewport
- viewport remains square
The latest measured browser viewport size was approximately:
`643 x 643`
## 3D Scene Interaction
The viewport supports:
- drag rotation
- mouse wheel zoom
- pinch zoom using two-pointer distance tracking
Idle auto-rotation was reduced to 25% of the original speed.
After user drag or zoom interaction, idle rotation restarts from zero and slowly accelerates back.
## Surface Shading
The initial continuous sphere looked flat. The surface shell was changed from unlit `MeshBasicMaterial` to lit `MeshLambertMaterial`.
Lighting added:
- ambient light
- warm green/yellow key light
- green fill light
- pale rim light
The CSS viewport grid overlay was removed because it created vertical bars on the right side of the scene.
## Current Verification State
The following checks were run repeatedly after changes:
- `npm install`
- `npm run build`
- local Vite preview at `http://127.0.0.1:3003/`
- browser DOM/layout checks
- browser console error checks
The current build passes:
`npm run build`
The latest bundle exists at:
`dist/diophantine-sphere.iife.js`
## Notes
The project is currently located in the wrong folder and will be moved. This log was created so the design and implementation context travels with the folder.
+11
View File
@@ -0,0 +1,11 @@
# Diophantine Sphere
Standalone Glitch University component that renders the teaching sequence from a continuous sphere to restricted integer rays. It can show a perfect surface `x² + y² + z² = r²`, an L1 surface `|x| + |y| + |z| = r`, a single complexity `|x| + |y| + |z| = C`, or the combined range `|x| + |y| + |z| in [1, C]`. Overlapping rays are combined and coloured by multiplicity.
## Development
- `npm install`
- `npm run dev`
- `npm run build`
The component follows the Glitch Component contract and registers itself as `window.GlitchComponents["diophantine-sphere"]` when loaded from the built IIFE bundle.
+3978
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
{
"componentId": "diophantine-sphere",
"displayName": "Diophantine Sphere",
"version": "1.0.0",
"folderName": "glitch_diophantine_sphere",
"packageName": "@glitch-components/diophantine-sphere",
"entry": "dist/diophantine-sphere.js",
"source": "src/index.tsx",
"tags": [
"glitch-component",
"diophantine",
"sphere",
"integer",
"visualization"
]
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diophantine Sphere</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/dev.tsx"></script>
</body>
</html>
+2994
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@glitch-components/diophantine-sphere",
"version": "1.0.0",
"description": "Diophantine Sphere glitch component for Glitch University",
"type": "module",
"main": "./dist/diophantine-sphere.js",
"module": "./dist/diophantine-sphere.js",
"types": "./src/types.ts",
"exports": {
".": {
"import": "./dist/diophantine-sphere.js",
"types": "./src/types.ts"
}
},
"files": [
"dist",
"src/types.ts",
"glitch.manifest.json"
],
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"dependencies": {
"three": "^0.181.2"
},
"devDependencies": {
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/three": "^0.181.0",
"@vitejs/plugin-react": "^4.7.0",
"leva": "^0.10.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"typescript": "^5.9.3",
"vite": "^6.4.1",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"keywords": [
"glitch",
"diophantine",
"sphere",
"integer",
"visualization"
],
"author": "Glitch.university",
"license": "MIT"
}
+845
View File
@@ -0,0 +1,845 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { CSSProperties, PointerEvent as ReactPointerEvent, WheelEvent as ReactWheelEvent } from 'react'
import * as THREE from 'three'
import styles from './styles.module.css'
import type { GlitchComponentProps } from './types'
interface ComponentParams {
allowNegative?: boolean
normalizeDistance?: boolean
radius?: number
complexity?: number
dotRadius?: number
complexityMode?: string
}
type ComplexityMode = 'surface' | 'single' | 'range'
interface RayBucket {
x: number
y: number
z: number
count: number
}
interface DragState {
x: number
y: number
rotationX: number
rotationY: number
}
interface PointerPoint {
x: number
y: number
}
interface PinchState {
distance: number
cameraDistance: number
}
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
const numberParam = (value: unknown, fallback: number) => (typeof value === 'number' ? value : fallback)
const MIN_CAMERA_DISTANCE = 6.8
const MAX_CAMERA_DISTANCE = 22
const DEFAULT_CAMERA_DISTANCE = 13
const IDLE_ROTATION_SPEED = 0.0005
const MAX_COMPLEXITY = 96
const MAX_DOT_RADIUS = 1.2
const gcd2 = (a: number, b: number): number => {
let left = Math.abs(a)
let right = Math.abs(b)
while (right !== 0) {
const next = left % right
left = right
right = next
}
return left
}
const gcd3 = (a: number, b: number, c: number) => gcd2(gcd2(a, b), c)
const createRange = (magnitude: number, allowNegative: boolean) => {
if (magnitude === 0) return [0]
return allowNegative ? [-magnitude, magnitude] : [magnitude]
}
const getPinchDistance = (points: PointerPoint[]) => {
if (points.length < 2) return 0
return Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y)
}
const colorRamp = (weight: number) => {
const cold = new THREE.Color('#7dff9a')
const warm = new THREE.Color('#f2ff6b')
const hot = new THREE.Color('#ffd166')
if (weight < 0.5) return cold.lerp(warm, weight / 0.5)
return warm.lerp(hot, (weight - 0.5) / 0.5)
}
const getComplexityMode = (value: unknown): ComplexityMode => {
if (value === 'surface' || value === 'single' || value === 'range') return value
return 'surface'
}
const getVectorRays = (
complexity: number,
radius: number,
allowNegative: boolean,
normalizeDistance: boolean,
mode: ComplexityMode
) => {
const rays = new Map<string, RayBucket>()
let totalVectors = 0
if (mode === 'surface') return { points: [], totalVectors, maxMultiplicity: 0 }
const startComplexity = mode === 'range' ? 1 : complexity
for (let c = startComplexity; c <= complexity; c += 1) {
for (let ax = 0; ax <= c; ax += 1) {
for (let ay = 0; ay <= c - ax; ay += 1) {
const az = c - ax - ay
for (const x of createRange(ax, allowNegative)) {
for (const y of createRange(ay, allowNegative)) {
for (const z of createRange(az, allowNegative)) {
const divisor = gcd3(x, y, z) || 1
const rayX = x / divisor
const rayY = y / divisor
const rayZ = z / divisor
const key = `${rayX},${rayY},${rayZ}`
const bucket = rays.get(key)
totalVectors += 1
if (bucket) {
bucket.count += 1
} else {
rays.set(key, { x: rayX, y: rayY, z: rayZ, count: 1 })
}
}
}
}
}
}
}
const points = Array.from(rays.values()).map((ray) => {
const length = normalizeDistance ? Math.hypot(ray.x, ray.y, ray.z) : Math.abs(ray.x) + Math.abs(ray.y) + Math.abs(ray.z)
return {
x: (ray.x / length) * radius,
y: (ray.y / length) * radius,
z: (ray.z / length) * radius,
count: ray.count
}
})
const maxMultiplicity = points.reduce((max, point) => Math.max(max, point.count), 1)
return { points, totalVectors, maxMultiplicity }
}
const createPointTexture = () => {
const canvas = document.createElement('canvas')
canvas.width = 96
canvas.height = 96
const ctx = canvas.getContext('2d')
if (ctx) {
const gradient = ctx.createRadialGradient(48, 48, 0, 48, 48, 48)
gradient.addColorStop(0, 'rgba(255,255,255,1)')
gradient.addColorStop(0.24, 'rgba(255,255,255,0.92)')
gradient.addColorStop(0.58, 'rgba(255,255,255,0.42)')
gradient.addColorStop(1, 'rgba(255,255,255,0)')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 96, 96)
}
return new THREE.CanvasTexture(canvas)
}
export default function Component({
config,
onComplete,
onProgress,
theme,
className,
host
}: GlitchComponentProps) {
const params = config.params as ComponentParams
const initialAllowNegative = typeof params.allowNegative === 'boolean' ? params.allowNegative : true
const initialNormalizeDistance = typeof params.normalizeDistance === 'boolean' ? params.normalizeDistance : true
const initialRadius = clamp(numberParam(params.radius, 4), 1, 8)
const initialComplexity = Math.round(clamp(numberParam(params.complexity, 8), 1, MAX_COMPLEXITY))
const initialDotRadius = clamp(numberParam(params.dotRadius, 0.17), 0.05, MAX_DOT_RADIUS)
const initialComplexityMode = getComplexityMode(params.complexityMode)
const [allowNegative, setAllowNegative] = useState(initialAllowNegative)
const [normalizeDistance, setNormalizeDistance] = useState(initialNormalizeDistance)
const [radius, setRadius] = useState(initialRadius)
const [complexity, setComplexity] = useState(initialComplexity)
const [dotRadius, setDotRadius] = useState(initialDotRadius)
const [complexityMode, setComplexityMode] = useState<ComplexityMode>(initialComplexityMode)
const [completed, setCompleted] = useState(false)
const mountRef = useRef<HTMLDivElement | null>(null)
const groupRef = useRef<THREE.Group | null>(null)
const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
const pointsRef = useRef<THREE.Points | null>(null)
const pointMaterialRef = useRef<THREE.PointsMaterial | null>(null)
const shellRef = useRef<THREE.Mesh | null>(null)
const l1ShellRef = useRef<THREE.Mesh | null>(null)
const dragRef = useRef<DragState | null>(null)
const pointersRef = useRef<Map<number, PointerPoint>>(new Map())
const pinchRef = useRef<PinchState | null>(null)
const cameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
const currentCameraDistanceRef = useRef(DEFAULT_CAMERA_DISTANCE)
const idleRotationFactorRef = useRef(1)
const rotationRef = useRef({ x: -0.45, y: 0.58 })
useEffect(() => {
setAllowNegative(initialAllowNegative)
}, [initialAllowNegative])
useEffect(() => {
setNormalizeDistance(initialNormalizeDistance)
}, [initialNormalizeDistance])
useEffect(() => {
setRadius(initialRadius)
}, [initialRadius])
useEffect(() => {
setComplexity(initialComplexity)
}, [initialComplexity])
useEffect(() => {
setDotRadius(initialDotRadius)
}, [initialDotRadius])
useEffect(() => {
setComplexityMode(initialComplexityMode)
}, [initialComplexityMode])
const sphereData = useMemo(
() => getVectorRays(complexity, radius, allowNegative, normalizeDistance, complexityMode),
[allowNegative, complexity, complexityMode, normalizeDistance, radius]
)
const { points, totalVectors, maxMultiplicity } = sphereData
const progress = Math.round(clamp((complexity / MAX_COMPLEXITY) * 100, 0, 100))
const duplicateCount = totalVectors - points.length
const isSurfaceMode = complexityMode === 'surface'
const isRangeMode = complexityMode === 'range'
const statRays = isSurfaceMode ? '∞' : String(points.length)
const statVectors = isSurfaceMode ? '∞' : String(totalVectors)
const statOverlap = isSurfaceMode ? 'surface' : String(duplicateCount)
const statMaxStack = isSurfaceMode ? '--' : String(maxMultiplicity)
const themeStyle = useMemo(() => ({
'--gc-primary': theme?.primary,
'--gc-accent': theme?.accent,
'--gc-bg': theme?.bg,
'--gc-bg-secondary': theme?.bgSecondary,
'--gc-text': theme?.text,
'--gc-text-muted': theme?.textMuted,
'--gc-border': theme?.border,
'--sphere-radius': radius
}) as CSSProperties, [radius, theme])
useEffect(() => {
onProgress?.(progress)
}, [onProgress, progress])
useEffect(() => {
const mount = mountRef.current
if (!mount) return undefined
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100)
camera.position.set(0, 0, currentCameraDistanceRef.current)
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
renderer.setClearColor(0x000000, 0)
mount.appendChild(renderer.domElement)
rendererRef.current = renderer
const group = new THREE.Group()
scene.add(group)
groupRef.current = group
const ambient = new THREE.AmbientLight(0xf1ffc4, 0.45)
scene.add(ambient)
const keyLight = new THREE.DirectionalLight(0xf2ff9b, 2.25)
keyLight.position.set(3.5, 4.5, 6)
scene.add(keyLight)
const fillLight = new THREE.DirectionalLight(0x79ff8c, 0.85)
fillLight.position.set(-5, -2, 3)
scene.add(fillLight)
const rimLight = new THREE.DirectionalLight(0xffffb2, 1.05)
rimLight.position.set(-2, 3, -5)
scene.add(rimLight)
const shellGeometry = new THREE.SphereGeometry(1, 96, 48)
const shellMaterial = new THREE.MeshLambertMaterial({
color: 0xcfff55,
emissive: 0x16260a,
emissiveIntensity: 0.18,
transparent: true,
opacity: 0.055,
wireframe: true,
depthWrite: false
})
const shell = new THREE.Mesh(shellGeometry, shellMaterial)
group.add(shell)
shellRef.current = shell
const l1ShellGeometry = new THREE.OctahedronGeometry(1, 0)
const l1ShellMaterial = new THREE.MeshLambertMaterial({
color: 0xcfff55,
emissive: 0x16260a,
emissiveIntensity: 0.16,
transparent: true,
opacity: 0.055,
wireframe: true,
depthWrite: false,
flatShading: true
})
const l1Shell = new THREE.Mesh(l1ShellGeometry, l1ShellMaterial)
l1Shell.visible = false
group.add(l1Shell)
l1ShellRef.current = l1Shell
const axisMaterial = new THREE.LineBasicMaterial({ color: 0xd7ff57, transparent: true, opacity: 0.12 })
const axisGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(0, -1, 0),
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(0, 0, -1),
new THREE.Vector3(0, 0, 1)
])
const axes = new THREE.LineSegments(axisGeometry, axisMaterial)
group.add(axes)
const pointMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: dotRadius,
map: createPointTexture(),
transparent: true,
opacity: 0.96,
depthWrite: false,
vertexColors: true,
blending: THREE.AdditiveBlending
})
pointMaterialRef.current = pointMaterial
const pointGeometry = new THREE.BufferGeometry()
const pointCloud = new THREE.Points(pointGeometry, pointMaterial)
group.add(pointCloud)
pointsRef.current = pointCloud
const resize = () => {
const rect = mount.getBoundingClientRect()
const width = Math.max(1, rect.width)
const height = Math.max(1, rect.height)
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
}
const resizeObserver = new ResizeObserver(resize)
resizeObserver.observe(mount)
resize()
let animationFrame = 0
const render = () => {
const rotation = rotationRef.current
currentCameraDistanceRef.current += (cameraDistanceRef.current - currentCameraDistanceRef.current) * 0.12
camera.position.z = currentCameraDistanceRef.current
camera.lookAt(0, 0, 0)
group.rotation.x += (rotation.x - group.rotation.x) * 0.08
group.rotation.y += (rotation.y - group.rotation.y) * 0.08
if (dragRef.current || pinchRef.current) {
idleRotationFactorRef.current = 0
} else {
idleRotationFactorRef.current += (1 - idleRotationFactorRef.current) * 0.006
group.rotation.y += IDLE_ROTATION_SPEED * idleRotationFactorRef.current
}
rotation.y = group.rotation.y
renderer.render(scene, camera)
animationFrame = window.requestAnimationFrame(render)
}
render()
return () => {
window.cancelAnimationFrame(animationFrame)
resizeObserver.disconnect()
mount.removeChild(renderer.domElement)
pointGeometry.dispose()
pointMaterial.map?.dispose()
pointMaterial.dispose()
axisGeometry.dispose()
axisMaterial.dispose()
shellGeometry.dispose()
shellMaterial.dispose()
l1ShellGeometry.dispose()
l1ShellMaterial.dispose()
renderer.dispose()
rendererRef.current = null
groupRef.current = null
pointsRef.current = null
pointMaterialRef.current = null
shellRef.current = null
l1ShellRef.current = null
}
}, [])
useEffect(() => {
const pointCloud = pointsRef.current
if (!pointCloud) return
const positions = new Float32Array(points.length * 3)
const colors = new Float32Array(points.length * 3)
points.forEach((point, index) => {
positions[index * 3] = point.x
positions[index * 3 + 1] = point.y
positions[index * 3 + 2] = point.z
const weight = maxMultiplicity <= 1 ? 0 : (point.count - 1) / (maxMultiplicity - 1)
const color = colorRamp(weight)
colors[index * 3] = color.r
colors[index * 3 + 1] = color.g
colors[index * 3 + 2] = color.b
})
const nextGeometry = new THREE.BufferGeometry()
nextGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
nextGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
pointCloud.geometry.dispose()
pointCloud.geometry = nextGeometry
}, [maxMultiplicity, points])
useEffect(() => {
shellRef.current?.scale.setScalar(radius)
l1ShellRef.current?.scale.setScalar(radius)
const axes = groupRef.current?.children.find((child) => child.type === 'LineSegments')
axes?.scale.setScalar(radius * 1.18)
}, [radius])
useEffect(() => {
if (pointsRef.current) {
pointsRef.current.visible = !isSurfaceMode
}
const euclideanShell = shellRef.current
const l1Shell = l1ShellRef.current
if (euclideanShell) {
euclideanShell.visible = normalizeDistance
}
if (l1Shell) {
l1Shell.visible = !normalizeDistance
}
for (const shell of [euclideanShell, l1Shell]) {
const shellMaterial = shell?.material
if (shellMaterial instanceof THREE.MeshLambertMaterial) {
shellMaterial.wireframe = !isSurfaceMode
shellMaterial.opacity = isSurfaceMode ? 0.34 : 0.055
shellMaterial.depthWrite = false
}
}
}, [isSurfaceMode, normalizeDistance])
useEffect(() => {
const accent = new THREE.Color(theme?.accent ?? '#4de1c1')
for (const shell of [shellRef.current, l1ShellRef.current]) {
const shellMaterial = shell?.material
if (shellMaterial instanceof THREE.MeshLambertMaterial) {
shellMaterial.color.copy(accent)
shellMaterial.emissive.set('#16260a')
}
}
}, [theme?.accent])
useEffect(() => {
if (pointMaterialRef.current) {
pointMaterialRef.current.size = dotRadius
}
}, [dotRadius])
const playClick = useCallback((target: string) => {
host?.playSound?.('ui.button_click', { target, component: config.name })
}, [config.name, host])
const finish = useCallback(() => {
if (completed) return
setCompleted(true)
playClick('complete')
onComplete({
success: true,
score: 100,
data: {
allowNegative,
normalizeDistance,
complexityMode,
radius,
dotRadius,
complexity,
rays: points.length,
vectors: totalVectors,
duplicateRays: duplicateCount,
maxMultiplicity,
definition: isSurfaceMode
? normalizeDistance
? 'A sphere is a continuous surface: infinitely many points at Euclidean distance r from the centre.'
: 'The L1 surface is a continuous octahedral surface where |x| + |y| + |z| = r.'
: isRangeMode
? 'A Diophantine sphere places one point on each distinct integer vector ray at radius r, combining complexities 1 through C.'
: 'A Diophantine sphere places one point on each distinct integer vector ray at radius r for a single complexity C.',
configId: config.id,
completedAt: new Date().toISOString()
}
})
}, [
allowNegative,
completed,
complexity,
config.id,
duplicateCount,
dotRadius,
isRangeMode,
isSurfaceMode,
maxMultiplicity,
normalizeDistance,
onComplete,
playClick,
points.length,
radius,
totalVectors,
complexityMode
])
const updateAllowNegative = useCallback(() => {
playClick('allow-negative')
setAllowNegative((value) => !value)
}, [playClick])
const updateNormalizeDistance = useCallback(() => {
playClick('normalize-distance')
setNormalizeDistance((value) => !value)
}, [playClick])
const updateRadius = useCallback((value: number) => {
playClick('radius')
setRadius(clamp(value, 1, 8))
}, [playClick])
const updateComplexity = useCallback((value: number) => {
playClick('complexity')
setComplexity(Math.round(clamp(value, 1, MAX_COMPLEXITY)))
}, [playClick])
const updateDotRadius = useCallback((value: number) => {
playClick('dot-radius')
setDotRadius(clamp(value, 0.05, MAX_DOT_RADIUS))
}, [playClick])
const updateComplexityMode = useCallback((nextMode: ComplexityMode) => {
playClick(`${nextMode}-mode`)
setComplexityMode(nextMode)
}, [playClick])
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
event.currentTarget.setPointerCapture(event.pointerId)
idleRotationFactorRef.current = 0
pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY })
const pointers = Array.from(pointersRef.current.values())
if (pointers.length >= 2) {
dragRef.current = null
pinchRef.current = {
distance: getPinchDistance(pointers),
cameraDistance: cameraDistanceRef.current
}
return
}
pinchRef.current = null
dragRef.current = {
x: event.clientX,
y: event.clientY,
rotationX: rotationRef.current.x,
rotationY: rotationRef.current.y
}
}, [])
const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (pointersRef.current.has(event.pointerId)) {
pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY })
}
const pointers = Array.from(pointersRef.current.values())
if (pointers.length >= 2) {
const pinch = pinchRef.current
if (!pinch) return
const nextDistance = getPinchDistance(pointers)
if (nextDistance > 0 && pinch.distance > 0) {
cameraDistanceRef.current = clamp(
pinch.cameraDistance * (pinch.distance / nextDistance),
MIN_CAMERA_DISTANCE,
MAX_CAMERA_DISTANCE
)
}
return
}
const drag = dragRef.current
if (!drag) return
rotationRef.current = {
x: clamp(drag.rotationX + (event.clientY - drag.y) * 0.008, -1.45, 1.45),
y: drag.rotationY + (event.clientX - drag.x) * 0.008
}
}, [])
const handlePointerEnd = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId)
}
pointersRef.current.delete(event.pointerId)
const pointers = Array.from(pointersRef.current.values())
if (pointers.length >= 2) {
pinchRef.current = {
distance: getPinchDistance(pointers),
cameraDistance: cameraDistanceRef.current
}
dragRef.current = null
return
}
pinchRef.current = null
const remainingPointer = pointers[0]
dragRef.current = remainingPointer
? {
x: remainingPointer.x,
y: remainingPointer.y,
rotationX: rotationRef.current.x,
rotationY: rotationRef.current.y
}
: null
}, [])
const handleWheel = useCallback((event: ReactWheelEvent<HTMLDivElement>) => {
event.preventDefault()
idleRotationFactorRef.current = 0
cameraDistanceRef.current = clamp(
cameraDistanceRef.current + event.deltaY * 0.012,
MIN_CAMERA_DISTANCE,
MAX_CAMERA_DISTANCE
)
}, [])
const rootClassName = className ? `${styles.wrapper} ${className}` : styles.wrapper
return (
<section className={rootClassName} style={themeStyle} aria-label="Diophantine Sphere visualizer">
<div className={styles.frame}>
<header className={styles.header}>
<div>
<span className={styles.kicker}>Integer rays</span>
<h2>Diophantine Sphere</h2>
</div>
</header>
<div className={styles.metricStrip} aria-label="Sphere statistics">
<div>
<span>rays</span>
<strong>{statRays}</strong>
</div>
<div>
<span>vectors</span>
<strong>{statVectors}</strong>
</div>
<div>
<span>overlap</span>
<strong>{statOverlap}</strong>
</div>
<div>
<span>max stack</span>
<strong>{statMaxStack}</strong>
</div>
</div>
<main className={styles.stageGrid}>
<section
className={styles.viewport}
aria-label="3D Diophantine Sphere"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
onWheel={handleWheel}
>
<div ref={mountRef} className={styles.canvasHost} />
<div className={styles.axisLabels} aria-hidden="true">
<span>x</span>
<span>y</span>
<span>z</span>
</div>
</section>
<aside className={styles.controlPanel}>
<div className={styles.modeControl} aria-label="Complexity mode">
<button
className={complexityMode === 'surface' ? styles.modeActive : ''}
type="button"
onClick={() => updateComplexityMode('surface')}
>
Surface
</button>
<button
className={complexityMode === 'single' ? styles.modeActive : ''}
type="button"
onClick={() => updateComplexityMode('single')}
>
Single C
</button>
<button
className={complexityMode === 'range' ? styles.modeActive : ''}
type="button"
onClick={() => updateComplexityMode('range')}
>
Range 1..C
</button>
</div>
<label className={styles.toggleRow}>
<span>
<strong>Allow negative values</strong>
<small>{allowNegative ? 'All octants' : 'Positive octant'}</small>
</span>
<input type="checkbox" checked={allowNegative} onChange={updateAllowNegative} />
</label>
<label className={styles.toggleRow}>
<span>
<strong>Normalize distance</strong>
<small>{normalizeDistance ? 'Euclidean r' : 'Complexity r'}</small>
</span>
<input type="checkbox" checked={normalizeDistance} onChange={updateNormalizeDistance} />
</label>
<label className={styles.sliderRow}>
<span>
<strong>D</strong>
<small>{radius.toFixed(2)}</small>
</span>
<div className={styles.sliderControl}>
<button type="button" onClick={() => updateRadius(radius - 0.25)} aria-label="Decrease radius">
-
</button>
<input
type="range"
min={1}
max={8}
step={0.25}
value={radius}
onInput={(event) => updateRadius(Number(event.currentTarget.value))}
onChange={(event) => updateRadius(Number(event.currentTarget.value))}
/>
<button type="button" onClick={() => updateRadius(radius + 0.25)} aria-label="Increase radius">
+
</button>
</div>
</label>
<label className={styles.sliderRow}>
<span>
<strong>C</strong>
<small>{complexity}</small>
</span>
<div className={styles.sliderControl}>
<button type="button" onClick={() => updateComplexity(complexity - 1)} aria-label="Decrease complexity">
-
</button>
<input
type="range"
min={1}
max={MAX_COMPLEXITY}
step={1}
value={complexity}
onInput={(event) => updateComplexity(Number(event.currentTarget.value))}
onChange={(event) => updateComplexity(Number(event.currentTarget.value))}
/>
<button type="button" onClick={() => updateComplexity(complexity + 1)} aria-label="Increase complexity">
+
</button>
</div>
</label>
<label className={styles.sliderRow}>
<span>
<strong>R</strong>
<small>{dotRadius.toFixed(2)}</small>
</span>
<div className={styles.sliderControl}>
<button type="button" onClick={() => updateDotRadius(dotRadius - 0.02)} aria-label="Decrease dot radius">
-
</button>
<input
type="range"
min={0.05}
max={MAX_DOT_RADIUS}
step={0.01}
value={dotRadius}
onInput={(event) => updateDotRadius(Number(event.currentTarget.value))}
onChange={(event) => updateDotRadius(Number(event.currentTarget.value))}
/>
<button type="button" onClick={() => updateDotRadius(dotRadius + 0.02)} aria-label="Increase dot radius">
+
</button>
</div>
</label>
<div className={styles.rayNote}>
<span className={styles.kicker}>colour rule</span>
<p>
{isSurfaceMode
? normalizeDistance
? 'Start with the continuous round surface: every Euclidean direction from the centre is present.'
: 'Unchecked distance normalization shows the continuous L1 surface that the integer grid lands on.'
: isRangeMode
? 'Cool points are single rays; gold and rose points mark directions hit by more vectors in the range.'
: 'Single-complexity mode exposes the triangular grid before lower complexities fill the gaps.'}
</p>
</div>
</aside>
</main>
<footer className={styles.footer}>
<span>
{isSurfaceMode
? normalizeDistance
? `Continuous sphere: infinitely many points at Euclidean radius ${radius.toFixed(2)}`
: `Continuous L1 surface: |x| + |y| + |z| = ${radius.toFixed(2)}`
: isRangeMode
? `Complexities 1-${complexity} collapse ${duplicateCount} overlapping vectors at ${normalizeDistance ? 'Euclidean' : 'complexity'} radius ${radius.toFixed(2)}`
: `Complexity ${complexity} places ${points.length} distinct rays at ${normalizeDistance ? 'Euclidean' : 'complexity'} radius ${radius.toFixed(2)}`}
</span>
<button className={styles.completeButton} type="button" onClick={finish} disabled={completed}>
{completed ? 'OK' : '✓'}
</button>
</footer>
</div>
</section>
)
}
+49
View File
@@ -0,0 +1,49 @@
@import url("https://fonts.googleapis.com/css2?family=Iceland&family=JetBrains+Mono:wght@400;600&family=Russo+One&display=swap");
:root {
--color-bg: #0a0a0f;
--color-bg-secondary: #12121a;
--color-text: #e8e8ec;
--color-text-muted: #9999a8;
--color-primary: #6366f1;
--color-primary-hover: #818cf8;
--color-accent: #22d3ee;
--color-accent-secondary: #a855f7;
--color-border: #2a2a3a;
--font-main: "Iceland", -apple-system, BlinkMacSystemFont, sans-serif;
--font-display: "Russo One", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", "Iceland", monospace;
--small-font: 1.875rem;
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 4rem;
--spacing-xl: 6rem;
--max-width: 1200px;
--border-radius: 12px;
--border-radius-sm: 8px;
--transition: 0.2s ease;
}
html,
body,
#root {
width: 100%;
min-height: 100%;
}
body {
margin: 0;
background:
radial-gradient(circle at top, rgb(99 102 241 / 18%), transparent 36%),
linear-gradient(180deg, #080810, #0d1018);
color: var(--color-text);
font-family: var(--font-main);
}
button,
input,
select,
textarea {
font: inherit;
}
+59
View File
@@ -0,0 +1,59 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Leva, useControls } from 'leva'
import Component from './Component'
import { metadata } from './index'
import './dev-theme.css'
function DevHarness() {
const params = useControls({
allowNegative: { value: true },
normalizeDistance: { value: true },
complexityMode: {
value: 'surface',
options: {
Surface: 'surface',
'Range 1..C': 'range',
'Single C': 'single'
}
},
radius: { value: 4, min: 1, max: 8, step: 0.25 },
complexity: { value: 8, min: 1, max: 96, step: 1 },
dotRadius: { value: 0.17, min: 0.05, max: 1.2, step: 0.01 }
})
return (
<div style={{ minHeight: '100vh' }}>
<Leva hidden />
<Component
config={{
id: 'dev',
name: metadata.name,
version: metadata.version,
params: params as Record<string, unknown>
}}
theme={{
primary: '#d7ff57',
accent: '#cfff55',
bg: '#050806',
bgSecondary: '#10150b',
text: '#edf8d4',
textMuted: '#9fb47d',
border: '#31411f'
}}
onProgress={(percent) => {
console.log('Progress:', percent)
}}
onComplete={(result) => {
console.log('Complete:', result)
}}
/>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<DevHarness />
</React.StrictMode>
)
+78
View File
@@ -0,0 +1,78 @@
import Component from './Component'
import type { GlitchComponentMetadata } from './types'
export default Component
export const metadata: GlitchComponentMetadata = {
name: 'diophantine-sphere',
displayName: 'Diophantine Sphere',
version: '1.0.0',
paramSchema: {
allowNegative: {
type: 'boolean',
label: 'Allow Negative Values',
default: true
},
normalizeDistance: {
type: 'boolean',
label: 'Normalize Distance',
default: true
},
complexityMode: {
type: 'select',
label: 'Complexity Mode',
default: 'surface',
options: [
{ value: 'surface', label: 'Surface' },
{ value: 'single', label: 'Single C' },
{ value: 'range', label: 'Range 1..C' }
]
},
radius: {
type: 'range',
label: 'D',
default: 4,
min: 1,
max: 8,
step: 0.25
},
complexity: {
type: 'range',
label: 'C',
default: 8,
min: 1,
max: 96,
step: 1
},
dotRadius: {
type: 'range',
label: 'R',
default: 0.17,
min: 0.05,
max: 1.2,
step: 0.01
}
},
defaultParams: {
allowNegative: true,
normalizeDistance: true,
complexityMode: 'surface',
radius: 4,
complexity: 8,
dotRadius: 0.17
}
}
export type {
GlitchComponentProps,
GlitchComponentConfig,
GlitchComponentResult,
GlitchTheme,
GlitchHostBridge
} from './types'
if (typeof window !== 'undefined') {
type GCRegistry = Record<string, { default: typeof Component; metadata: GlitchComponentMetadata }>
;(window as unknown as { GlitchComponents: GCRegistry }).GlitchComponents ??= {}
;(window as unknown as { GlitchComponents: GCRegistry }).GlitchComponents[metadata.name] = { default: Component, metadata }
}
+445
View File
@@ -0,0 +1,445 @@
.wrapper {
--gc-bg: var(--color-bg, #050806);
--gc-bg-secondary: var(--color-bg-secondary, #10150b);
--gc-text: var(--color-text, #edf8d4);
--gc-text-muted: var(--color-text-muted, #9fb47d);
--gc-primary: var(--color-primary, #d7ff57);
--gc-accent: var(--color-accent, #cfff55);
--gc-border: var(--color-border, #31411f);
--gc-font-main: var(--font-main, system-ui, sans-serif);
--gc-font-display: var(--font-display, var(--gc-font-main));
--gc-font-mono: var(--font-mono, monospace);
--gc-shell-padding: clamp(8px, 2vw, 18px);
width: 100%;
min-height: 100dvh;
display: grid;
place-items: center;
padding: var(--gc-shell-padding);
box-sizing: border-box;
}
.wrapper *,
.wrapper *::before,
.wrapper *::after {
box-sizing: inherit;
}
.frame {
position: relative;
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) * 1.9));
max-height: calc(100dvh - (var(--gc-shell-padding) * 2));
aspect-ratio: 1.9 / 1;
display: grid;
grid-template-rows: minmax(0, 1fr);
overflow: hidden;
padding: 1em;
color: var(--gc-text);
font-family: var(--gc-font-main);
font-size: clamp(11px, 1.16cqw, 20px);
line-height: 1.2;
background:
linear-gradient(145deg, color-mix(in srgb, var(--gc-bg) 90%, #2a310b), var(--gc-bg-secondary)),
var(--gc-bg);
border: 1px solid color-mix(in srgb, var(--gc-border) 74%, transparent);
box-shadow:
inset 0 0 0 1px rgb(215 255 87 / 5%),
0 18px 48px rgb(0 0 0 / 34%);
container-type: inline-size;
}
.header,
.metricStrip,
.footer {
display: flex;
align-items: center;
}
.header,
.footer {
justify-content: space-between;
gap: 1em;
}
.header {
position: absolute;
z-index: 5;
left: 1em;
top: 0.9em;
width: min(25em, 34%);
pointer-events: none;
}
.header h2 {
margin: 0.1em 0 0;
font-family: var(--gc-font-display);
font-size: 1.64em;
line-height: 1;
letter-spacing: 0;
}
.kicker {
display: none;
color: color-mix(in srgb, var(--gc-accent) 78%, white);
font-family: var(--gc-font-mono);
font-size: 0.62em;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.metricStrip {
position: absolute;
z-index: 5;
left: 1em;
bottom: 1em;
width: min(25em, 34%);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.4em;
}
.metricStrip div {
min-width: 0;
padding: 0.55em 0.68em;
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
background: rgb(4 9 13 / 58%);
}
.metricStrip span,
.footer span {
color: var(--gc-text-muted);
font-family: var(--gc-font-mono);
font-size: 0.62em;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.metricStrip strong {
display: block;
margin-top: 0.12em;
color: var(--gc-accent);
font-family: var(--gc-font-display);
font-size: 1.3em;
line-height: 0.96;
letter-spacing: 0;
}
.stageGrid {
min-height: 0;
grid-row: 1;
display: grid;
grid-template-columns: minmax(16em, 0.48fr) minmax(0, 1fr);
align-items: center;
gap: 0.7em;
}
.viewport,
.controlPanel,
.footer {
background: rgb(4 9 13 / 56%);
}
.viewport {
position: relative;
grid-column: 2;
min-width: 0;
min-height: 0;
width: min(100%, calc(100dvh - 5.2em));
aspect-ratio: 1;
justify-self: end;
overflow: hidden;
cursor: grab;
touch-action: none;
}
.viewport:active {
cursor: grabbing;
}
.canvasHost {
position: absolute;
inset: 0;
}
.canvasHost canvas {
display: block;
width: 100%;
height: 100%;
}
.viewport::before {
content: none;
position: absolute;
inset: 0;
pointer-events: none;
}
.viewport::after {
content: "";
position: absolute;
inset: 1.1em;
pointer-events: none;
border: 1px solid color-mix(in srgb, var(--gc-accent) 18%, transparent);
}
.axisLabels {
position: absolute;
inset: 0.8em;
display: flex;
align-items: flex-end;
justify-content: flex-start;
gap: 0.38em;
pointer-events: none;
}
.axisLabels span {
width: 1.8em;
height: 1.8em;
display: grid;
place-items: center;
border: 1px solid color-mix(in srgb, var(--gc-border) 80%, transparent);
background: rgb(5 10 14 / 76%);
color: color-mix(in srgb, var(--gc-text-muted) 82%, white);
font-family: var(--gc-font-mono);
font-size: 0.68em;
font-weight: 800;
text-transform: uppercase;
}
.controlPanel {
grid-column: 1;
grid-row: 1;
min-width: 0;
min-height: 0;
display: grid;
align-content: start;
gap: 0.55em;
padding: 0.7em;
margin-top: 4.2em;
margin-bottom: 6.8em;
}
.equation,
.rayNote {
padding-bottom: 0.55em;
border-bottom: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
}
.equation p {
margin: 0;
color: var(--gc-text);
font-family: var(--gc-font-display);
font-size: 1.25em;
letter-spacing: 0;
}
.rayNote p {
margin: 0.42em 0 0;
color: color-mix(in srgb, var(--gc-text-muted) 86%, white);
font-size: 0.78em;
line-height: 1.24;
}
.rayNote,
.footer {
display: none;
}
.toggleRow,
.sliderRow {
display: grid;
gap: 0.4em;
padding: 0.58em;
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
background: rgb(5 10 14 / 54%);
}
.modeControl {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.28em;
padding: 0.28em;
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
background: rgb(5 10 14 / 54%);
}
.modeControl button {
min-width: 0;
min-height: 2.15em;
border: 1px solid transparent;
background: transparent;
color: var(--gc-text-muted);
font: inherit;
font-family: var(--gc-font-mono);
font-size: 0.6em;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
}
.modeControl button:hover {
border-color: color-mix(in srgb, var(--gc-accent) 38%, var(--gc-border));
}
.modeControl .modeActive {
border-color: color-mix(in srgb, var(--gc-accent) 70%, white);
background: color-mix(in srgb, var(--gc-accent) 18%, rgb(5 10 14));
color: color-mix(in srgb, var(--gc-accent) 86%, white);
}
.toggleRow {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
}
.toggleRow span,
.sliderRow span {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.55em;
}
.toggleRow strong,
.sliderRow strong {
min-width: 0;
color: var(--gc-text);
font-family: var(--gc-font-mono);
font-size: 0.68em;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.toggleRow small,
.sliderRow small {
flex: 0 0 auto;
color: var(--gc-accent);
font-family: var(--gc-font-mono);
font-size: 0.68em;
font-weight: 800;
}
.toggleRow input[type="checkbox"] {
width: 2.6em;
height: 1.45em;
margin: 0;
accent-color: var(--gc-accent);
cursor: pointer;
}
.sliderControl {
min-width: 0;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.45em;
}
.sliderControl button {
width: 1.9em;
height: 1.9em;
display: grid;
place-items: center;
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
background: rgb(5 10 14 / 74%);
color: var(--gc-text);
font: inherit;
font-family: var(--gc-font-mono);
font-size: 0.72em;
font-weight: 900;
cursor: pointer;
}
.sliderControl button:hover {
border-color: color-mix(in srgb, var(--gc-accent) 62%, var(--gc-border));
}
.sliderControl input[type="range"] {
width: 100%;
min-width: 0;
accent-color: var(--gc-accent);
cursor: pointer;
}
.completeButton {
min-width: 3em;
min-height: 2.35em;
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
background: color-mix(in srgb, var(--gc-primary) 24%, rgb(5 9 18));
color: var(--gc-text);
font: inherit;
font-family: var(--gc-font-mono);
font-size: 0.7em;
font-weight: 800;
cursor: pointer;
transition: transform 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
.completeButton:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--gc-accent) 62%, var(--gc-border));
}
.completeButton:disabled {
cursor: default;
opacity: 0.68;
transform: none;
}
@media (orientation: portrait) {
.frame {
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) / 1.7));
aspect-ratio: 1 / 1.7;
font-size: clamp(10px, 3.15cqw, 16px);
}
.header {
align-items: flex-start;
flex-direction: column;
gap: 0.55em;
}
.metricStrip {
width: 100%;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.metricStrip div {
min-width: 0;
}
.stageGrid {
grid-template-columns: 1fr;
grid-template-rows: auto minmax(0, 1fr);
}
.viewport {
grid-column: 1;
grid-row: 2;
width: min(100%, 54dvh);
justify-self: center;
}
.controlPanel {
grid-column: 1;
grid-row: 1;
grid-template-columns: 1fr 1fr;
padding: 0.72em;
}
.equation,
.rayNote {
display: none;
}
.toggleRow,
.sliderRow {
padding: 0.58em;
}
}
+59
View File
@@ -0,0 +1,59 @@
export interface GlitchComponentConfig {
id: string
name: string
version: string
params: Record<string, unknown>
}
export interface GlitchHostBridge {
playSound?: (id: string, payload?: Record<string, unknown>) => void
stopSound?: (id: string, payload?: Record<string, unknown>) => void
emit?: (type: string, payload?: unknown) => void
}
export interface GlitchTheme {
primary: string
accent: string
bg: string
bgSecondary: string
text: string
textMuted: string
border: string
}
export interface GlitchComponentResult {
success: boolean
score?: number
data?: unknown
error?: string
}
export interface GlitchComponentProps {
config: GlitchComponentConfig
onComplete: (result: GlitchComponentResult) => void
onProgress?: (percent: number) => void
theme?: GlitchTheme
className?: string
host?: GlitchHostBridge
}
export interface ParamSchema {
[key: string]: {
type: 'number' | 'string' | 'boolean' | 'color' | 'select' | 'range'
label?: string
description?: string
default: unknown
options?: { value: string | number; label: string }[]
min?: number
max?: number
step?: number
}
}
export interface GlitchComponentMetadata {
name: string
displayName: string
version: string
paramSchema: ParamSchema
defaultParams: Record<string, unknown>
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"types": [
"vite/client"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src"
]
}
+32
View File
@@ -0,0 +1,32 @@
import { resolve } from 'path'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
export default defineConfig(({ mode }) => ({
plugins: [react(), cssInjectedByJsPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.tsx'),
name: 'GlitchComponent',
fileName: 'diophantine-sphere',
formats: ['iife']
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
},
assetFileNames: 'assets/[name][extname]'
}
},
sourcemap: true,
minify: mode === 'production'
},
server: {
port: 3002,
open: true
}
}))