Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Bubble Feedback
|
||||||
|
|
||||||
|
Standalone Glitch University component that shows bubbles as self-amplifying feedback loops: output becomes input, and each completed loop pushes the chart higher.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- `npm install`
|
||||||
|
- `npm run dev`
|
||||||
|
- `npm run build`
|
||||||
|
|
||||||
|
The component follows the Glitch Component contract and registers itself as `window.GlitchComponents["bubble-feedback"]` when loaded from the built IIFE bundle.
|
||||||
Vendored
+8
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"componentId": "bubble-feedback",
|
||||||
|
"displayName": "Bubble Feedback",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"folderName": "glitch-bubble-simulator",
|
||||||
|
"packageName": "@glitch-components/bubble-feedback",
|
||||||
|
"entry": "dist/bubble-feedback.js",
|
||||||
|
"source": "src/index.tsx",
|
||||||
|
"tags": [
|
||||||
|
"glitch-component",
|
||||||
|
"bubble",
|
||||||
|
"feedback",
|
||||||
|
"simulation"
|
||||||
|
]
|
||||||
|
}
|
||||||
+12
@@ -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>Bubble Feedback</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/dev.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2919
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@glitch-components/bubble-feedback",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Bubble Feedback glitch component for Glitch University",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/bubble-feedback.js",
|
||||||
|
"module": "./dist/bubble-feedback.js",
|
||||||
|
"types": "./src/types.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/bubble-feedback.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"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"@vitejs/plugin-react": "5.1.1",
|
||||||
|
"leva": "^0.10.1",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "7.2.4",
|
||||||
|
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"glitch",
|
||||||
|
"bubble",
|
||||||
|
"feedback",
|
||||||
|
"simulation",
|
||||||
|
"component"
|
||||||
|
],
|
||||||
|
"author": "Glitch.university",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
import styles from './styles.module.css'
|
||||||
|
import type { GlitchComponentProps } from './types'
|
||||||
|
|
||||||
|
type ScenarioKey = 'ai-stock' | 'string-theory'
|
||||||
|
|
||||||
|
interface LoopScenario {
|
||||||
|
key: ScenarioKey
|
||||||
|
title: string
|
||||||
|
shortLabel: string
|
||||||
|
subject: string
|
||||||
|
outputLabel: string
|
||||||
|
chartLabel: string
|
||||||
|
color: string
|
||||||
|
events: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomScenario {
|
||||||
|
key?: string
|
||||||
|
title?: string
|
||||||
|
shortLabel?: string
|
||||||
|
subject?: string
|
||||||
|
outputLabel?: string
|
||||||
|
chartLabel?: string
|
||||||
|
color?: string
|
||||||
|
events?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentParams {
|
||||||
|
scenario?: string
|
||||||
|
scenarios?: CustomScenario[]
|
||||||
|
animationSpeed?: number
|
||||||
|
feedbackStrength?: number
|
||||||
|
correctionPressure?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Palette {
|
||||||
|
accent: string
|
||||||
|
text: string
|
||||||
|
muted: string
|
||||||
|
grid: string
|
||||||
|
scenario: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SCENARIOS: LoopScenario[] = [
|
||||||
|
{
|
||||||
|
key: 'ai-stock',
|
||||||
|
title: 'AI Stock Bubble',
|
||||||
|
shortLabel: 'AI STOCK',
|
||||||
|
subject: 'Market price becomes evidence',
|
||||||
|
outputLabel: 'price',
|
||||||
|
chartLabel: 'price expectation',
|
||||||
|
color: '#f0c45c',
|
||||||
|
events: [
|
||||||
|
'Stock price rises because people buy',
|
||||||
|
'Stronger financial position lets the company borrow more',
|
||||||
|
'Borrowing more results in larger AI investment',
|
||||||
|
'Larger AI investment increases AI race winning probability',
|
||||||
|
'AI race winning increases future earnings expectation'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'string-theory',
|
||||||
|
title: 'String Theory Field Bubble',
|
||||||
|
shortLabel: 'STRING',
|
||||||
|
subject: 'Activity becomes evidence',
|
||||||
|
outputLabel: 'field activity',
|
||||||
|
chartLabel: 'field promise',
|
||||||
|
color: '#74d6ff',
|
||||||
|
events: [
|
||||||
|
'People choose String Theory for doctoral theses because of job opportunities',
|
||||||
|
'String Theory gets lots of new researchers',
|
||||||
|
'Researchers publish research and cite other string theorists',
|
||||||
|
'Activity and citations become proxy for how promising a research field is',
|
||||||
|
'Promising translates to successful grants',
|
||||||
|
'Grants become open positions'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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 createScenario = (scenario: CustomScenario, index: number): LoopScenario | null => {
|
||||||
|
if (!Array.isArray(scenario.events) || scenario.events.length < 2) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: (scenario.key ?? `custom-${index}`) as ScenarioKey,
|
||||||
|
title: scenario.title ?? `Custom Loop ${index + 1}`,
|
||||||
|
shortLabel: scenario.shortLabel ?? `LOOP ${index + 1}`,
|
||||||
|
subject: scenario.subject ?? 'Output becomes input',
|
||||||
|
outputLabel: scenario.outputLabel ?? 'output',
|
||||||
|
chartLabel: scenario.chartLabel ?? 'amplification',
|
||||||
|
color: scenario.color ?? '#8ee6a8',
|
||||||
|
events: scenario.events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScenarios = (params: ComponentParams) => {
|
||||||
|
const customScenarios = Array.isArray(params.scenarios)
|
||||||
|
? params.scenarios.map(createScenario).filter((scenario): scenario is LoopScenario => Boolean(scenario))
|
||||||
|
: []
|
||||||
|
|
||||||
|
return customScenarios.length > 0 ? customScenarios : DEFAULT_SCENARIOS
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBubbleValue = (cycle: number, feedbackStrength: number, correctionPressure: number) => {
|
||||||
|
const drift = cycle * Math.max(0.02, feedbackStrength - 0.52) * 0.22
|
||||||
|
const compounding = Math.exp(cycle * Math.max(0, feedbackStrength - 0.86) * 0.11)
|
||||||
|
const correction = Math.sin(cycle * 1.38) * correctionPressure * 0.42
|
||||||
|
return clamp(0.1 + drift + compounding * 0.18 - correction, 0.08, 2.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChart(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
cycle: number,
|
||||||
|
feedbackStrength: number,
|
||||||
|
correctionPressure: number,
|
||||||
|
palette: Palette
|
||||||
|
) {
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
const scale = window.devicePixelRatio || 1
|
||||||
|
const width = rect.width
|
||||||
|
const height = rect.height
|
||||||
|
|
||||||
|
canvas.width = Math.floor(width * scale)
|
||||||
|
canvas.height = Math.floor(height * scale)
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
ctx.setTransform(scale, 0, 0, scale, 0, 0)
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const padX = width * 0.08
|
||||||
|
const padY = height * 0.14
|
||||||
|
const chartW = width - padX * 2
|
||||||
|
const chartH = height - padY * 2
|
||||||
|
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeStyle = palette.grid
|
||||||
|
for (let index = 0; index <= 4; index += 1) {
|
||||||
|
const y = padY + (index / 4) * chartH
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(padX, y)
|
||||||
|
ctx.lineTo(width - padX, y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = 80
|
||||||
|
const points = Array.from({ length: samples }, (_, index) => {
|
||||||
|
const sampleCycle = Math.max(0, cycle - 7 + (index / (samples - 1)) * 7)
|
||||||
|
const value = getBubbleValue(sampleCycle, feedbackStrength, correctionPressure)
|
||||||
|
return {
|
||||||
|
x: padX + (index / (samples - 1)) * chartW,
|
||||||
|
y: padY + chartH - (value / 2.7) * chartH
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(0, padY, 0, height - padY)
|
||||||
|
gradient.addColorStop(0, palette.scenario)
|
||||||
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
points.forEach((point, index) => {
|
||||||
|
if (index === 0) ctx.moveTo(point.x, point.y)
|
||||||
|
else ctx.lineTo(point.x, point.y)
|
||||||
|
})
|
||||||
|
ctx.lineTo(width - padX, height - padY)
|
||||||
|
ctx.lineTo(padX, height - padY)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fillStyle = gradient
|
||||||
|
ctx.globalAlpha = 0.22
|
||||||
|
ctx.fill()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
points.forEach((point, index) => {
|
||||||
|
if (index === 0) ctx.moveTo(point.x, point.y)
|
||||||
|
else ctx.lineTo(point.x, point.y)
|
||||||
|
})
|
||||||
|
ctx.strokeStyle = palette.scenario
|
||||||
|
ctx.lineWidth = Math.max(2, width * 0.006)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
const latest = points[points.length - 1]
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(latest.x, latest.y, Math.max(5, width * 0.014), 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = palette.accent
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({
|
||||||
|
config,
|
||||||
|
onComplete,
|
||||||
|
onProgress,
|
||||||
|
theme,
|
||||||
|
className,
|
||||||
|
host
|
||||||
|
}: GlitchComponentProps) {
|
||||||
|
const params = config.params as ComponentParams
|
||||||
|
const scenarios = useMemo(() => getScenarios(params), [params])
|
||||||
|
const [scenarioKey, setScenarioKey] = useState(() => String(params.scenario ?? scenarios[0].key))
|
||||||
|
const [running, setRunning] = useState(true)
|
||||||
|
const [cycle, setCycle] = useState(0)
|
||||||
|
const [completed, setCompleted] = useState(false)
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
|
const lastTickRef = useRef(0)
|
||||||
|
|
||||||
|
const animationSpeed = clamp(numberParam(params.animationSpeed, 1), 0.25, 3)
|
||||||
|
const feedbackStrength = clamp(numberParam(params.feedbackStrength, 1.18), 0.55, 1.8)
|
||||||
|
const correctionPressure = clamp(numberParam(params.correctionPressure, 0.18), 0, 0.65)
|
||||||
|
const scenario = scenarios.find((item) => item.key === scenarioKey) ?? scenarios[0]
|
||||||
|
const phase = cycle % 1
|
||||||
|
const eventIndex = Math.floor(phase * scenario.events.length) % scenario.events.length
|
||||||
|
const nextEventIndex = (eventIndex + 1) % scenario.events.length
|
||||||
|
const bubbleValue = getBubbleValue(cycle, feedbackStrength, correctionPressure)
|
||||||
|
const pressure = Math.round(clamp((bubbleValue / 2.7) * 100, 0, 100))
|
||||||
|
const progress = Math.round(clamp((cycle / 12) * 100, 0, 100))
|
||||||
|
|
||||||
|
const palette = useMemo<Palette>(() => ({
|
||||||
|
accent: theme?.accent ?? '#22d3ee',
|
||||||
|
text: theme?.text ?? '#e8e8ec',
|
||||||
|
muted: theme?.textMuted ?? '#9999a8',
|
||||||
|
grid: 'rgba(232, 232, 236, 0.13)',
|
||||||
|
scenario: scenario.color
|
||||||
|
}), [scenario.color, theme?.accent, theme?.text, theme?.textMuted])
|
||||||
|
|
||||||
|
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,
|
||||||
|
'--bubble-color': scenario.color,
|
||||||
|
'--rotation': `${phase * 360}deg`
|
||||||
|
}) as CSSProperties, [phase, scenario.color, theme])
|
||||||
|
|
||||||
|
const redraw = useCallback(() => {
|
||||||
|
if (!canvasRef.current) return
|
||||||
|
drawChart(canvasRef.current, cycle, feedbackStrength, correctionPressure, palette)
|
||||||
|
}, [correctionPressure, cycle, feedbackStrength, palette])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScenarioKey(String(params.scenario ?? scenarios[0].key))
|
||||||
|
}, [params.scenario, scenarios])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCycle(0)
|
||||||
|
}, [scenarioKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onProgress?.(progress)
|
||||||
|
}, [onProgress, progress])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
redraw()
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return undefined
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(redraw)
|
||||||
|
resizeObserver.observe(canvas)
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
|
}, [redraw])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let animationFrame = 0
|
||||||
|
|
||||||
|
const step = (timestamp: number) => {
|
||||||
|
const interval = 34 / animationSpeed
|
||||||
|
if (running && timestamp - lastTickRef.current > interval) {
|
||||||
|
setCycle((current) => (current >= 12 ? 0 : current + 0.0125))
|
||||||
|
lastTickRef.current = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = window.requestAnimationFrame(step)
|
||||||
|
return () => window.cancelAnimationFrame(animationFrame)
|
||||||
|
}, [animationSpeed, running])
|
||||||
|
|
||||||
|
const playClick = useCallback((target: string) => {
|
||||||
|
host?.playSound?.('ui.button_click', { target, component: config.name })
|
||||||
|
}, [config.name, host])
|
||||||
|
|
||||||
|
const chooseScenario = useCallback((key: ScenarioKey) => {
|
||||||
|
playClick(key)
|
||||||
|
setRunning(true)
|
||||||
|
setScenarioKey(key)
|
||||||
|
}, [playClick])
|
||||||
|
|
||||||
|
const finish = useCallback(() => {
|
||||||
|
if (completed) return
|
||||||
|
|
||||||
|
setCompleted(true)
|
||||||
|
playClick('complete')
|
||||||
|
onComplete({
|
||||||
|
success: true,
|
||||||
|
score: 100,
|
||||||
|
data: {
|
||||||
|
scenario: scenario.key,
|
||||||
|
cycle,
|
||||||
|
pressure,
|
||||||
|
activeEvent: scenario.events[eventIndex],
|
||||||
|
nextEvent: scenario.events[nextEventIndex],
|
||||||
|
definition: 'A bubble is a self-amplifying feedback loop: output becomes input.',
|
||||||
|
configId: config.id,
|
||||||
|
completedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [completed, config.id, cycle, eventIndex, nextEventIndex, onComplete, playClick, pressure, scenario])
|
||||||
|
|
||||||
|
const rootClassName = className ? `${styles.wrapper} ${className}` : styles.wrapper
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={rootClassName} style={themeStyle} aria-label="Bubble feedback simulator">
|
||||||
|
<div className={styles.frame}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<span className={styles.kicker}>Feedback loop</span>
|
||||||
|
<h2>A bubble is output used as input</h2>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls} aria-label="Simulation controls">
|
||||||
|
<button className={styles.iconButton} type="button" onClick={() => setRunning((value) => !value)}>
|
||||||
|
{running ? 'II' : '>'}
|
||||||
|
</button>
|
||||||
|
{scenarios.map((item) => (
|
||||||
|
<button
|
||||||
|
className={`${styles.modeButton} ${item.key === scenario.key ? styles.modeActive : ''}`}
|
||||||
|
key={item.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => chooseScenario(item.key)}
|
||||||
|
>
|
||||||
|
{item.shortLabel}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className={styles.stageGrid}>
|
||||||
|
<section className={styles.loopPanel} aria-label={`${scenario.title} loop`}>
|
||||||
|
<div className={styles.loopHeader}>
|
||||||
|
<div>
|
||||||
|
<span className={styles.kicker}>{scenario.title}</span>
|
||||||
|
<strong>{scenario.subject}</strong>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cycleReadout}>
|
||||||
|
<span>cycle</span>
|
||||||
|
<strong>{cycle.toFixed(1)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.loop}>
|
||||||
|
<div className={styles.ring} aria-hidden="true" />
|
||||||
|
<div className={styles.pulse} aria-hidden="true" />
|
||||||
|
<div className={styles.core}>
|
||||||
|
<canvas ref={canvasRef} className={styles.canvas} aria-label={`${scenario.chartLabel} chart`} />
|
||||||
|
<div className={styles.coreOverlay}>
|
||||||
|
<span>{scenario.chartLabel}</span>
|
||||||
|
<strong>{pressure}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{scenario.events.map((event, index) => {
|
||||||
|
const angle = (index / scenario.events.length) * Math.PI * 2 - Math.PI / 2
|
||||||
|
const x = 50 + Math.cos(angle) * 32
|
||||||
|
const y = 50 + Math.sin(angle) * 32
|
||||||
|
const labelX = Math.cos(angle) * 5.4
|
||||||
|
const labelY = Math.sin(angle) * 4.5
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.loopStation}
|
||||||
|
key={event}
|
||||||
|
style={{
|
||||||
|
left: `${x}%`,
|
||||||
|
top: `${y}%`,
|
||||||
|
'--label-x': `${labelX}em`,
|
||||||
|
'--label-y': `${labelY}em`
|
||||||
|
} as CSSProperties}
|
||||||
|
>
|
||||||
|
<div className={`${styles.loopNode} ${index === eventIndex ? styles.nodeActive : ''}`}>
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<p className={`${styles.stationLabel} ${index === eventIndex ? styles.labelActive : ''}`}>
|
||||||
|
{event}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className={styles.eventPanel}>
|
||||||
|
<span className={styles.kicker}>Now feeding back</span>
|
||||||
|
<h3>{scenario.events[eventIndex]}</h3>
|
||||||
|
<div className={styles.nextEvent}>
|
||||||
|
<span>next</span>
|
||||||
|
<p>{scenario.events[nextEventIndex]}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.definition}>
|
||||||
|
<span>definition</span>
|
||||||
|
<p>A bubble is a self-amplifying feedback loop: output becomes input.</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<input
|
||||||
|
aria-label="Loop cycle"
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={12}
|
||||||
|
step={0.05}
|
||||||
|
value={cycle}
|
||||||
|
onChange={(event) => {
|
||||||
|
playClick('cycle')
|
||||||
|
setRunning(false)
|
||||||
|
setCycle(Number(event.currentTarget.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className={styles.completeButton} type="button" onClick={finish} disabled={completed}>
|
||||||
|
{completed ? 'OK' : '✓'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
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({
|
||||||
|
scenario: {
|
||||||
|
value: 'ai-stock',
|
||||||
|
options: {
|
||||||
|
'AI Stock Bubble': 'ai-stock',
|
||||||
|
'String Theory Field Bubble': 'string-theory'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animationSpeed: { value: 1, min: 0.25, max: 3, step: 0.25 },
|
||||||
|
feedbackStrength: { value: 1.18, min: 0.55, max: 1.8, step: 0.05 },
|
||||||
|
correctionPressure: { value: 0.18, min: 0, max: 0.65, step: 0.05 }
|
||||||
|
})
|
||||||
|
|
||||||
|
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: '#6366f1',
|
||||||
|
accent: '#22d3ee',
|
||||||
|
bg: '#0a0a0f',
|
||||||
|
bgSecondary: '#12121a',
|
||||||
|
text: '#e8e8ec',
|
||||||
|
textMuted: '#9999a8',
|
||||||
|
border: '#2a2a3a'
|
||||||
|
}}
|
||||||
|
onProgress={(percent) => {
|
||||||
|
console.log('Progress:', percent)
|
||||||
|
}}
|
||||||
|
onComplete={(result) => {
|
||||||
|
console.log('Complete:', result)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<DevHarness />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import Component from './Component'
|
||||||
|
import type { GlitchComponentMetadata } from './types'
|
||||||
|
|
||||||
|
export default Component
|
||||||
|
|
||||||
|
export const metadata: GlitchComponentMetadata = {
|
||||||
|
name: 'bubble-feedback',
|
||||||
|
displayName: 'Bubble Feedback',
|
||||||
|
version: '1.0.0',
|
||||||
|
paramSchema: {
|
||||||
|
scenario: {
|
||||||
|
type: 'select',
|
||||||
|
label: 'Scenario',
|
||||||
|
default: 'ai-stock',
|
||||||
|
options: [
|
||||||
|
{ value: 'ai-stock', label: 'AI Stock Bubble' },
|
||||||
|
{ value: 'string-theory', label: 'String Theory Field Bubble' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
animationSpeed: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Animation Speed',
|
||||||
|
default: 1,
|
||||||
|
min: 0.25,
|
||||||
|
max: 3,
|
||||||
|
step: 0.25
|
||||||
|
},
|
||||||
|
feedbackStrength: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Feedback Strength',
|
||||||
|
default: 1.18,
|
||||||
|
min: 0.55,
|
||||||
|
max: 1.8,
|
||||||
|
step: 0.05
|
||||||
|
},
|
||||||
|
correctionPressure: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Correction Pressure',
|
||||||
|
default: 0.18,
|
||||||
|
min: 0,
|
||||||
|
max: 0.65,
|
||||||
|
step: 0.05
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultParams: {
|
||||||
|
scenario: 'ai-stock',
|
||||||
|
animationSpeed: 1,
|
||||||
|
feedbackStrength: 1.18,
|
||||||
|
correctionPressure: 0.18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
.wrapper {
|
||||||
|
--gc-bg: var(--color-bg, #0a0a0f);
|
||||||
|
--gc-bg-secondary: var(--color-bg-secondary, #12121a);
|
||||||
|
--gc-text: var(--color-text, #e8e8ec);
|
||||||
|
--gc-text-muted: var(--color-text-muted, #9999a8);
|
||||||
|
--gc-primary: var(--color-primary, #6366f1);
|
||||||
|
--gc-accent: var(--color-accent, #22d3ee);
|
||||||
|
--gc-border: var(--color-border, #2a2a3a);
|
||||||
|
--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)) * 16 / 9));
|
||||||
|
max-height: calc(100dvh - (var(--gc-shell-padding) * 2));
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
gap: 0.65em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1em;
|
||||||
|
color: var(--gc-text);
|
||||||
|
font-family: var(--gc-font-main);
|
||||||
|
font-size: clamp(10px, 1.08cqw, 18px);
|
||||||
|
line-height: 1.2;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 16% 12%, color-mix(in srgb, var(--bubble-color) 22%, transparent), transparent 28%),
|
||||||
|
radial-gradient(circle at 82% 16%, color-mix(in srgb, var(--gc-accent) 18%, transparent), transparent 30%),
|
||||||
|
linear-gradient(145deg, var(--gc-bg), var(--gc-bg-secondary));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 74%, transparent);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(255 255 255 / 4%),
|
||||||
|
0 18px 48px rgb(0 0 0 / 34%);
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.controls,
|
||||||
|
.loopHeader,
|
||||||
|
.footer,
|
||||||
|
.cycleReadout {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.loopHeader,
|
||||||
|
.footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
margin: 0.1em 0 0;
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 1.42em;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: block;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
gap: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton,
|
||||||
|
.modeButton,
|
||||||
|
.completeButton {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
|
||||||
|
color: var(--gc-text);
|
||||||
|
font: inherit;
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.66em;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 120ms ease, border-color 120ms ease, background-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton:hover,
|
||||||
|
.modeButton:hover,
|
||||||
|
.completeButton:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--gc-accent) 62%, var(--gc-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton {
|
||||||
|
min-width: 3.5em;
|
||||||
|
min-height: 2.6em;
|
||||||
|
background: rgb(10 14 24 / 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeButton {
|
||||||
|
min-height: 2.6em;
|
||||||
|
padding: 0 0.8em;
|
||||||
|
background: rgb(10 14 24 / 58%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeActive {
|
||||||
|
color: #090b10;
|
||||||
|
background: linear-gradient(90deg, var(--bubble-color), color-mix(in srgb, var(--gc-accent) 42%, var(--bubble-color)));
|
||||||
|
border-color: rgb(255 255 255 / 56%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageGrid {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.32fr) minmax(12em, 0.68fr);
|
||||||
|
gap: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loopPanel,
|
||||||
|
.eventPanel {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 76%, transparent);
|
||||||
|
background: rgb(5 9 18 / 56%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loopPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 0.3em;
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loopHeader strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
font-size: 0.86em;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cycleReadout {
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.16em;
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.66em;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cycleReadout strong {
|
||||||
|
color: var(--bubble-color);
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 2.1em;
|
||||||
|
line-height: 0.9;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loop {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 34em);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring {
|
||||||
|
position: absolute;
|
||||||
|
inset: 18%;
|
||||||
|
border: 0.28em solid color-mix(in srgb, var(--bubble-color) 44%, transparent);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 1.7em rgb(255 255 255 / 5%),
|
||||||
|
0 0 1.6em color-mix(in srgb, var(--bubble-color) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -0.55em;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px dashed color-mix(in srgb, var(--gc-text) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
position: absolute;
|
||||||
|
inset: 50%;
|
||||||
|
width: 1.05em;
|
||||||
|
height: 1.05em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--gc-accent);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 0.4em color-mix(in srgb, var(--gc-accent) 14%, transparent),
|
||||||
|
0 0 1.4em color-mix(in srgb, var(--gc-accent) 80%, transparent);
|
||||||
|
transform: rotate(var(--rotation)) translateY(-10.7em) translate(-50%, -50%);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core {
|
||||||
|
position: absolute;
|
||||||
|
inset: 34%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgb(7 11 17 / 88%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coreOverlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 12% 13%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coreOverlay span {
|
||||||
|
max-width: 9em;
|
||||||
|
color: color-mix(in srgb, var(--gc-text-muted) 88%, white);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.58em;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coreOverlay strong {
|
||||||
|
color: var(--bubble-color);
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 1.28em;
|
||||||
|
line-height: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, color-mix(in srgb, var(--bubble-color) 8%, transparent), transparent 40%),
|
||||||
|
rgb(7 11 17 / 78%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loopStation {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loopNode {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 2.4em;
|
||||||
|
height: 2.4em;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 80%, transparent);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgb(9 13 22 / 94%);
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.78em;
|
||||||
|
font-weight: 800;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: color 160ms ease, border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeActive {
|
||||||
|
color: #071018;
|
||||||
|
background: var(--bubble-color);
|
||||||
|
border-color: rgb(255 255 255 / 72%);
|
||||||
|
box-shadow: 0 0 1.2em color-mix(in srgb, var(--bubble-color) 54%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stationLabel {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 12.4em;
|
||||||
|
max-height: 4.6em;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.48em 0.58em;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
|
||||||
|
background: rgb(5 9 18 / 82%);
|
||||||
|
color: color-mix(in srgb, var(--gc-text-muted) 86%, white);
|
||||||
|
font-size: 0.62em;
|
||||||
|
line-height: 1.14;
|
||||||
|
transform: translate(calc(-50% + var(--label-x)), calc(-50% + var(--label-y)));
|
||||||
|
transition: color 160ms ease, border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelActive {
|
||||||
|
border-color: color-mix(in srgb, var(--bubble-color) 70%, var(--gc-border));
|
||||||
|
background: color-mix(in srgb, var(--bubble-color) 18%, rgb(5 9 18));
|
||||||
|
color: var(--gc-text);
|
||||||
|
box-shadow: 0 0 1.1em color-mix(in srgb, var(--bubble-color) 26%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1em;
|
||||||
|
padding: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--gc-text);
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 1.18em;
|
||||||
|
line-height: 1.04;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextEvent {
|
||||||
|
padding-top: 0.75em;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextEvent span,
|
||||||
|
.definition span {
|
||||||
|
color: var(--bubble-color);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.62em;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextEvent p,
|
||||||
|
.definition p {
|
||||||
|
margin: 0.35em 0 0;
|
||||||
|
color: color-mix(in srgb, var(--gc-text-muted) 86%, white);
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition {
|
||||||
|
padding-top: 0.75em;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
padding: 0.62em 0.8em;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 76%, transparent);
|
||||||
|
background: rgb(5 9 18 / 54%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer input {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--bubble-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completeButton {
|
||||||
|
min-width: 3em;
|
||||||
|
min-height: 2.35em;
|
||||||
|
padding: 0 0.8em;
|
||||||
|
background: color-mix(in srgb, var(--gc-primary) 28%, rgb(5 9 18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.completeButton:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.68;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.frame {
|
||||||
|
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) / 2));
|
||||||
|
aspect-ratio: 1 / 2;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
font-size: clamp(10px, 3.15cqw, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.55em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3.4em repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loop {
|
||||||
|
width: min(100%, 29em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
transform: rotate(var(--rotation)) translateY(-9.2em) translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel {
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel h3 {
|
||||||
|
font-size: 0.96em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextEvent p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stationLabel {
|
||||||
|
width: 10.8em;
|
||||||
|
max-height: 3.9em;
|
||||||
|
font-size: 0.54em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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: 'bubble-feedback',
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user