1576 lines
51 KiB
Python
1576 lines
51 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
generate_code_patterns_frontend.py — Generate 1K problem→solution training pairs
|
||
|
|
for frontend & creative code (Three.js, HTML/CSS/JS, playground UI, gallery, games).
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python3 training/scripts/generate_code_patterns_frontend.py
|
||
|
|
python3 training/scripts/generate_code_patterns_frontend.py --count 1000 --output training-data/code-patterns-frontend-&-creative.jsonl
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import random
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
random.seed(42)
|
||
|
|
|
||
|
|
# ── Variation pools ──
|
||
|
|
COLORS = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#ffeaa7", "#dfe6e9",
|
||
|
|
"#fd79a8", "#a29bfe", "#00b894", "#e17055", "#74b9ff", "#55efc4",
|
||
|
|
"#fab1a0", "#fdcb6e", "#6c5ce7", "#0984e3", "#00cec9", "#e84393"]
|
||
|
|
|
||
|
|
NAMES = ["hero", "scene", "viewer", "canvas", "stage", "world", "panel", "card",
|
||
|
|
"grid", "list", "item", "box", "circle", "shape", "model", "sprite",
|
||
|
|
"player", "enemy", "obstacle", "target", "button", "slider", "menu"]
|
||
|
|
|
||
|
|
ADJECTIVES = ["dynamic", "interactive", "responsive", "animated", "stylish",
|
||
|
|
"minimal", "modern", "elegant", "bold", "soft", "vibrant", "dark"]
|
||
|
|
|
||
|
|
SIZES = ["100px", "200px", "300px", "50%", "100%", "80vh", "60vw", "400px", "250px"]
|
||
|
|
|
||
|
|
SPEEDS = [0.01, 0.02, 0.05, 0.1, 0.005, 0.03, 0.015]
|
||
|
|
|
||
|
|
COUNTS = [3, 5, 8, 10, 12, 15, 20, 50]
|
||
|
|
|
||
|
|
|
||
|
|
def pick(pool, rng=random):
|
||
|
|
return rng.choice(pool)
|
||
|
|
|
||
|
|
|
||
|
|
def picks(pool, k, rng=random):
|
||
|
|
return rng.sample(pool, k=min(k, len(pool)))
|
||
|
|
|
||
|
|
|
||
|
|
# ── Domain: Three.js ──
|
||
|
|
def threejs_templates():
|
||
|
|
t = []
|
||
|
|
t.append((
|
||
|
|
"Create a Three.js scene with a rotating colored cube",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function createCubeScene(containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found: ' + containerId);
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
scene.background = new THREE.Color('{color1}');
|
||
|
|
|
||
|
|
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||
|
|
camera.position.z = 5;
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||
|
|
const material = new THREE.MeshStandardMaterial({ color: '{color2}', roughness: 0.3, metalness: 0.1 });
|
||
|
|
const cube = new THREE.Mesh(geometry, material);
|
||
|
|
scene.add(cube);
|
||
|
|
|
||
|
|
const light = new THREE.DirectionalLight(0xffffff, 1);
|
||
|
|
light.position.set(5, 5, 5);
|
||
|
|
scene.add(light);
|
||
|
|
scene.add(new THREE.AmbientLight(0x404040));
|
||
|
|
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
cube.rotation.x += {speed};
|
||
|
|
cube.rotation.y += {speed};
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
window.addEventListener('resize', () => {
|
||
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
||
|
|
camera.updateProjectionMatrix();
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
});
|
||
|
|
|
||
|
|
return { scene, camera, renderer, cube };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createCubeScene();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Load a GLTF 3D model in Three.js with orbit controls",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||
|
|
|
||
|
|
async function loadModel(url, containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found');
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
scene.background = new THREE.Color('{color1}');
|
||
|
|
scene.fog = new THREE.Fog('{color1}', 10, 50);
|
||
|
|
|
||
|
|
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100);
|
||
|
|
camera.position.set(0, 2, 5);
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
renderer.shadowMap.enabled = true;
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||
|
|
controls.enableDamping = true;
|
||
|
|
controls.dampingFactor = 0.05;
|
||
|
|
|
||
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
||
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
||
|
|
dirLight.position.set(5, 10, 7);
|
||
|
|
dirLight.castShadow = true;
|
||
|
|
scene.add(dirLight);
|
||
|
|
|
||
|
|
const loader = new GLTFLoader();
|
||
|
|
try {
|
||
|
|
const gltf = await loader.loadAsync(url);
|
||
|
|
const model = gltf.scene;
|
||
|
|
model.traverse(child => { if (child.isMesh) child.castShadow = true; });
|
||
|
|
scene.add(model);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to load model:', err);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
controls.update();
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
return { scene, camera, controls };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: loadModel('/models/character.glb');'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a particle system in Three.js with random motion",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function createParticles(count = {count}, containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found');
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||
|
|
camera.position.z = 30;
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
const geometry = new THREE.BufferGeometry();
|
||
|
|
const positions = new Float32Array(count * 3);
|
||
|
|
const velocities = [];
|
||
|
|
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
positions[i * 3] = (Math.random() - 0.5) * 50;
|
||
|
|
positions[i * 3 + 1] = (Math.random() - 0.5) * 50;
|
||
|
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 50;
|
||
|
|
velocities.push({
|
||
|
|
x: (Math.random() - 0.5) * {speed},
|
||
|
|
y: (Math.random() - 0.5) * {speed},
|
||
|
|
z: (Math.random() - 0.5) * {speed}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
|
|
const material = new THREE.PointsMaterial({ color: '{color2}', size: 0.5, transparent: true, opacity: 0.8 });
|
||
|
|
const particles = new THREE.Points(geometry, material);
|
||
|
|
scene.add(particles);
|
||
|
|
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
const pos = geometry.attributes.position.array;
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
pos[i * 3] += velocities[i].x;
|
||
|
|
pos[i * 3 + 1] += velocities[i].y;
|
||
|
|
pos[i * 3 + 2] += velocities[i].z;
|
||
|
|
if (Math.abs(pos[i * 3]) > 25) velocities[i].x *= -1;
|
||
|
|
if (Math.abs(pos[i * 3 + 1]) > 25) velocities[i].y *= -1;
|
||
|
|
if (Math.abs(pos[i * 3 + 2]) > 25) velocities[i].z *= -1;
|
||
|
|
}
|
||
|
|
geometry.attributes.position.needsUpdate = true;
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
return { scene, particles, renderer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createParticles(200);'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a reflective sphere with environment mapping in Three.js",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function createReflectiveSphere(containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found');
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||
|
|
camera.position.z = 3;
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||
|
|
const envScene = new THREE.Scene();
|
||
|
|
envScene.background = new THREE.Color('{color1}');
|
||
|
|
envScene.add(new THREE.Mesh(
|
||
|
|
new THREE.SphereGeometry(10, 32, 32),
|
||
|
|
new THREE.MeshBasicMaterial({ color: '{color2}', side: THREE.BackSide })
|
||
|
|
));
|
||
|
|
const envMap = pmremGenerator.fromScene(envScene).texture;
|
||
|
|
|
||
|
|
const geometry = new THREE.SphereGeometry(1, 64, 64);
|
||
|
|
const material = new THREE.MeshPhysicalMaterial({
|
||
|
|
color: '{color3}',
|
||
|
|
metalness: 1.0,
|
||
|
|
roughness: 0.1,
|
||
|
|
envMap,
|
||
|
|
envMapIntensity: 1.0
|
||
|
|
});
|
||
|
|
const sphere = new THREE.Mesh(geometry, material);
|
||
|
|
scene.add(sphere);
|
||
|
|
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
sphere.rotation.y += {speed};
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
return { scene, sphere, renderer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createReflectiveSphere();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build a Three.js first-person camera controller with WASD movement",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function createFPSController(containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found');
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
scene.background = new THREE.Color('{color1}');
|
||
|
|
|
||
|
|
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||
|
|
camera.position.y = 1.7;
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
// Floor
|
||
|
|
const floor = new THREE.Mesh(
|
||
|
|
new THREE.PlaneGeometry(50, 50),
|
||
|
|
new THREE.MeshStandardMaterial({ color: '{color2}' })
|
||
|
|
);
|
||
|
|
floor.rotation.x = -Math.PI / 2;
|
||
|
|
floor.receiveShadow = true;
|
||
|
|
scene.add(floor);
|
||
|
|
|
||
|
|
scene.add(new THREE.AmbientLight(0x404040));
|
||
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||
|
|
dirLight.position.set(5, 10, 5);
|
||
|
|
scene.add(dirLight);
|
||
|
|
|
||
|
|
const keys = { w: false, a: false, s: false, d: false };
|
||
|
|
const velocity = new THREE.Vector3();
|
||
|
|
const direction = new THREE.Vector3();
|
||
|
|
|
||
|
|
document.addEventListener('keydown', (e) => { if (keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = true; });
|
||
|
|
document.addEventListener('keyup', (e) => { if (keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = false; });
|
||
|
|
|
||
|
|
container.addEventListener('click', () => container.requestPointerLock());
|
||
|
|
document.addEventListener('mousemove', (e) => {
|
||
|
|
if (document.pointerLockElement === container) {
|
||
|
|
camera.rotation.y -= e.movementX * 0.002;
|
||
|
|
camera.rotation.x -= e.movementY * 0.002;
|
||
|
|
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const clock = new THREE.Clock();
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
const delta = clock.getDelta();
|
||
|
|
velocity.x -= velocity.x * 10 * delta;
|
||
|
|
velocity.z -= velocity.z * 10 * delta;
|
||
|
|
direction.z = Number(keys.w) - Number(keys.s);
|
||
|
|
direction.x = Number(keys.a) - Number(keys.d);
|
||
|
|
direction.normalize();
|
||
|
|
if (keys.w || keys.s) velocity.z -= direction.z * 40 * delta;
|
||
|
|
if (keys.a || keys.d) velocity.x -= direction.x * 40 * delta;
|
||
|
|
camera.translateX(-velocity.x * delta);
|
||
|
|
camera.translateZ(-velocity.z * delta);
|
||
|
|
camera.position.y = 1.7;
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
return { scene, camera, renderer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createFPSController();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create an animated torus knot with wireframe overlay in Three.js",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function createTorusKnot(containerId = 'canvas-container') {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (!container) throw new Error('Container not found');
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
scene.background = new THREE.Color('{color1}');
|
||
|
|
|
||
|
|
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||
|
|
camera.position.z = 5;
|
||
|
|
|
||
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||
|
|
container.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
const geometry = new THREE.TorusKnotGeometry(1, 0.3, 100, 16);
|
||
|
|
const material = new THREE.MeshStandardMaterial({ color: '{color2}', roughness: 0.4, metalness: 0.3 });
|
||
|
|
const torus = new THREE.Mesh(geometry, material);
|
||
|
|
scene.add(torus);
|
||
|
|
|
||
|
|
const wireGeo = new THREE.WireframeGeometry(geometry);
|
||
|
|
const wireMat = new THREE.LineBasicMaterial({ color: '{color3}' });
|
||
|
|
const wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||
|
|
torus.add(wireframe);
|
||
|
|
|
||
|
|
scene.add(new THREE.AmbientLight(0x404040));
|
||
|
|
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
|
||
|
|
pointLight.position.set(2, 3, 4);
|
||
|
|
scene.add(pointLight);
|
||
|
|
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
torus.rotation.x += {speed};
|
||
|
|
torus.rotation.y += {speed} * 0.7;
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
return { scene, torus, renderer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createTorusKnot();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Raycast from mouse to detect 3D object clicks in Three.js",
|
||
|
|
'''import * as THREE from 'three';
|
||
|
|
|
||
|
|
function setupRaycasting(scene, camera, renderer, onIntersect) {
|
||
|
|
const raycaster = new THREE.Raycaster();
|
||
|
|
const mouse = new THREE.Vector2();
|
||
|
|
|
||
|
|
renderer.domElement.addEventListener('click', (event) => {
|
||
|
|
const rect = renderer.domElement.getBoundingClientRect();
|
||
|
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
|
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
|
|
||
|
|
raycaster.setFromCamera(mouse, camera);
|
||
|
|
const intersects = raycaster.intersectObjects(scene.children, true);
|
||
|
|
|
||
|
|
if (intersects.length > 0) {
|
||
|
|
const hit = intersects[0];
|
||
|
|
console.log('Clicked:', hit.object.name || hit.object.uuid);
|
||
|
|
if (typeof onIntersect === 'function') onIntersect(hit);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage example:
|
||
|
|
// const mesh = new THREE.Mesh(geometry, material);
|
||
|
|
// mesh.name = 'clickable-box';
|
||
|
|
// scene.add(mesh);
|
||
|
|
// setupRaycasting(scene, camera, renderer, (hit) => {
|
||
|
|
// hit.object.material.color.setHex(Math.random() * 0xffffff);
|
||
|
|
// });'''
|
||
|
|
))
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
# ── Domain: HTML/CSS/JS ──
|
||
|
|
def htmlcss_templates():
|
||
|
|
t = []
|
||
|
|
t.append((
|
||
|
|
"Build a responsive CSS grid layout with auto-fit columns",
|
||
|
|
'''/* Responsive grid layout */
|
||
|
|
.grid-container {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax({size}, 1fr));
|
||
|
|
gap: 1.5rem;
|
||
|
|
padding: 2rem;
|
||
|
|
max-width: 1200px;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid-item {
|
||
|
|
background: {color1};
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid-item:hover {
|
||
|
|
transform: translateY(-4px);
|
||
|
|
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 600px) {
|
||
|
|
.grid-container {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
}'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a sticky navigation bar that changes style on scroll",
|
||
|
|
'''/* Sticky nav styles */
|
||
|
|
.nav-bar {
|
||
|
|
position: sticky;
|
||
|
|
top: 0;
|
||
|
|
z-index: 1000;
|
||
|
|
background: transparent;
|
||
|
|
padding: 1rem 2rem;
|
||
|
|
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-bar.scrolled {
|
||
|
|
background: rgba(255, 255, 255, 0.95);
|
||
|
|
backdrop-filter: blur(10px);
|
||
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* JavaScript */
|
||
|
|
function initStickyNav(navSelector = '.nav-bar') {
|
||
|
|
const nav = document.querySelector(navSelector);
|
||
|
|
if (!nav) throw new Error('Nav element not found');
|
||
|
|
|
||
|
|
function onScroll() {
|
||
|
|
if (window.scrollY > 50) {
|
||
|
|
nav.classList.add('scrolled');
|
||
|
|
} else {
|
||
|
|
nav.classList.remove('scrolled');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||
|
|
onScroll(); // Initialize state
|
||
|
|
return nav;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: initStickyNav();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Implement debounced search input with fetch API",
|
||
|
|
'''async function debouncedSearch(inputSelector, endpoint, renderFn, delay = 300) {
|
||
|
|
const input = document.querySelector(inputSelector);
|
||
|
|
if (!input) throw new Error('Input element not found');
|
||
|
|
|
||
|
|
let timeoutId = null;
|
||
|
|
let controller = null;
|
||
|
|
|
||
|
|
input.addEventListener('input', (e) => {
|
||
|
|
const query = e.target.value.trim();
|
||
|
|
clearTimeout(timeoutId);
|
||
|
|
if (controller) controller.abort();
|
||
|
|
|
||
|
|
if (!query) {
|
||
|
|
renderFn([]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
timeoutId = setTimeout(async () => {
|
||
|
|
controller = new AbortController();
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`, {
|
||
|
|
signal: controller.signal
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
|
|
const data = await res.json();
|
||
|
|
renderFn(data);
|
||
|
|
} catch (err) {
|
||
|
|
if (err.name !== 'AbortError') {
|
||
|
|
console.error('Search failed:', err);
|
||
|
|
renderFn([], err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, delay);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// debouncedSearch('#search', '/api/search', (results, err) => {
|
||
|
|
// if (err) return showError(err);
|
||
|
|
// updateDOM(results);
|
||
|
|
// });'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a CSS-only modal with backdrop blur and focus trap",
|
||
|
|
'''/* Modal styles */
|
||
|
|
.modal-overlay {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background: rgba(0, 0, 0, 0.5);
|
||
|
|
backdrop-filter: blur(4px);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
opacity: 0;
|
||
|
|
visibility: hidden;
|
||
|
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-overlay.active {
|
||
|
|
opacity: 1;
|
||
|
|
visibility: visible;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content {
|
||
|
|
background: white;
|
||
|
|
border-radius: 16px;
|
||
|
|
padding: 2rem;
|
||
|
|
max-width: 500px;
|
||
|
|
width: 90%;
|
||
|
|
transform: scale(0.9);
|
||
|
|
transition: transform 0.3s ease;
|
||
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-overlay.active .modal-content {
|
||
|
|
transform: scale(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* JavaScript for focus trap and keyboard */
|
||
|
|
function initModal(triggerSelector, modalSelector) {
|
||
|
|
const trigger = document.querySelector(triggerSelector);
|
||
|
|
const modal = document.querySelector(modalSelector);
|
||
|
|
if (!trigger || !modal) throw new Error('Modal elements not found');
|
||
|
|
|
||
|
|
const content = modal.querySelector('.modal-content');
|
||
|
|
const focusables = content.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||
|
|
|
||
|
|
function open() {
|
||
|
|
modal.classList.add('active');
|
||
|
|
(focusables[0] || content).focus();
|
||
|
|
document.addEventListener('keydown', onKey);
|
||
|
|
}
|
||
|
|
|
||
|
|
function close() {
|
||
|
|
modal.classList.remove('active');
|
||
|
|
document.removeEventListener('keydown', onKey);
|
||
|
|
trigger.focus();
|
||
|
|
}
|
||
|
|
|
||
|
|
function onKey(e) {
|
||
|
|
if (e.key === 'Escape') close();
|
||
|
|
if (e.key === 'Tab' && focusables.length > 0) {
|
||
|
|
const first = focusables[0];
|
||
|
|
const last = focusables[focusables.length - 1];
|
||
|
|
if (e.shiftKey && document.activeElement === first) {
|
||
|
|
e.preventDefault();
|
||
|
|
last.focus();
|
||
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||
|
|
e.preventDefault();
|
||
|
|
first.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
trigger.addEventListener('click', open);
|
||
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
|
||
|
|
|
||
|
|
return { open, close };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: initModal('#open-modal', '#my-modal');'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build a custom dropdown select with keyboard navigation",
|
||
|
|
'''function createCustomSelect(selectElement) {
|
||
|
|
if (!(selectElement instanceof HTMLSelectElement)) {
|
||
|
|
throw new TypeError('Expected HTMLSelectElement');
|
||
|
|
}
|
||
|
|
|
||
|
|
const wrapper = document.createElement('div');
|
||
|
|
wrapper.className = 'custom-select';
|
||
|
|
wrapper.style.position = 'relative';
|
||
|
|
wrapper.style.width = selectElement.offsetWidth + 'px';
|
||
|
|
|
||
|
|
const trigger = document.createElement('button');
|
||
|
|
trigger.type = 'button';
|
||
|
|
trigger.className = 'select-trigger';
|
||
|
|
trigger.textContent = selectElement.options[selectElement.selectedIndex]?.text || 'Select...';
|
||
|
|
trigger.setAttribute('aria-haspopup', 'listbox');
|
||
|
|
|
||
|
|
const list = document.createElement('ul');
|
||
|
|
list.className = 'select-options';
|
||
|
|
list.setAttribute('role', 'listbox');
|
||
|
|
list.style.cssText = 'position:absolute;top:100%;left:0;right:0;max-height:200px;overflow:auto;list-style:none;margin:0;padding:0;border:1px solid #ccc;background:#fff;z-index:100;display:none;';
|
||
|
|
|
||
|
|
Array.from(selectElement.options).forEach((opt, i) => {
|
||
|
|
const li = document.createElement('li');
|
||
|
|
li.textContent = opt.text;
|
||
|
|
li.setAttribute('role', 'option');
|
||
|
|
li.setAttribute('aria-selected', String(opt.selected));
|
||
|
|
li.dataset.value = opt.value;
|
||
|
|
li.style.padding = '0.5rem 1rem';
|
||
|
|
li.style.cursor = 'pointer';
|
||
|
|
li.addEventListener('click', () => {
|
||
|
|
selectElement.value = opt.value;
|
||
|
|
trigger.textContent = opt.text;
|
||
|
|
close();
|
||
|
|
selectElement.dispatchEvent(new Event('change'));
|
||
|
|
});
|
||
|
|
list.appendChild(li);
|
||
|
|
});
|
||
|
|
|
||
|
|
wrapper.appendChild(trigger);
|
||
|
|
wrapper.appendChild(list);
|
||
|
|
selectElement.style.display = 'none';
|
||
|
|
selectElement.parentNode.insertBefore(wrapper, selectElement);
|
||
|
|
|
||
|
|
let activeIndex = -1;
|
||
|
|
function open() {
|
||
|
|
list.style.display = 'block';
|
||
|
|
trigger.setAttribute('aria-expanded', 'true');
|
||
|
|
activeIndex = Array.from(selectElement.options).findIndex(o => o.selected);
|
||
|
|
}
|
||
|
|
function close() {
|
||
|
|
list.style.display = 'none';
|
||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
||
|
|
}
|
||
|
|
|
||
|
|
trigger.addEventListener('click', () => {
|
||
|
|
list.style.display === 'block' ? close() : open();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('click', (e) => {
|
||
|
|
if (!wrapper.contains(e.target)) close();
|
||
|
|
});
|
||
|
|
|
||
|
|
trigger.addEventListener('keydown', (e) => {
|
||
|
|
const items = list.querySelectorAll('li');
|
||
|
|
if (e.key === 'ArrowDown') { open(); activeIndex = Math.min(activeIndex + 1, items.length - 1); items[activeIndex]?.focus(); e.preventDefault(); }
|
||
|
|
if (e.key === 'ArrowUp') { open(); activeIndex = Math.max(activeIndex - 1, 0); items[activeIndex]?.focus(); e.preventDefault(); }
|
||
|
|
if (e.key === 'Enter' || e.key === ' ') { if (list.style.display === 'block' && items[activeIndex]) items[activeIndex].click(); else open(); e.preventDefault(); }
|
||
|
|
if (e.key === 'Escape') close();
|
||
|
|
});
|
||
|
|
|
||
|
|
return wrapper;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createCustomSelect(document.getElementById('my-select'));'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Implement smooth scroll-to-section with intersection observer highlighting",
|
||
|
|
'''function initScrollSpy(navSelector, sectionSelector, options = {}) {
|
||
|
|
const navLinks = document.querySelectorAll(`${navSelector} a[href^="#"]`);
|
||
|
|
const sections = document.querySelectorAll(sectionSelector);
|
||
|
|
if (!navLinks.length || !sections.length) {
|
||
|
|
console.warn('Scroll spy: no nav links or sections found');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const offset = options.offset || 80;
|
||
|
|
|
||
|
|
// Smooth scroll on click
|
||
|
|
navLinks.forEach(link => {
|
||
|
|
link.addEventListener('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const targetId = link.getAttribute('href').slice(1);
|
||
|
|
const target = document.getElementById(targetId);
|
||
|
|
if (target) {
|
||
|
|
window.scrollTo({ top: target.offsetTop - offset, behavior: 'smooth' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Intersection observer for active state
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
navLinks.forEach(l => l.classList.remove('active'));
|
||
|
|
const active = document.querySelector(`${navSelector} a[href="#${entry.target.id}"]`);
|
||
|
|
if (active) active.classList.add('active');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, { rootMargin: `-${offset}px 0px -60% 0px` });
|
||
|
|
|
||
|
|
sections.forEach(section => observer.observe(section));
|
||
|
|
return observer;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: initScrollSpy('.side-nav', 'section[data-section]');'''
|
||
|
|
))
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
# ── Domain: Playground UI ──
|
||
|
|
def playground_templates():
|
||
|
|
t = []
|
||
|
|
t.append((
|
||
|
|
"Create a draggable range slider with real-time value display",
|
||
|
|
'''function createRangeSlider(container, options = {}) {
|
||
|
|
const { min = 0, max = 100, step = 1, value = 50, onChange } = options;
|
||
|
|
container = typeof container === 'string' ? document.querySelector(container) : container;
|
||
|
|
if (!container) throw new Error('Slider container not found');
|
||
|
|
|
||
|
|
const wrapper = document.createElement('div');
|
||
|
|
wrapper.className = 'range-slider';
|
||
|
|
wrapper.style.cssText = 'display:flex;align-items:center;gap:1rem;font-family:sans-serif;';
|
||
|
|
|
||
|
|
const input = document.createElement('input');
|
||
|
|
input.type = 'range';
|
||
|
|
input.min = min;
|
||
|
|
input.max = max;
|
||
|
|
input.step = step;
|
||
|
|
input.value = value;
|
||
|
|
input.style.flex = '1';
|
||
|
|
|
||
|
|
const valueDisplay = document.createElement('span');
|
||
|
|
valueDisplay.className = 'slider-value';
|
||
|
|
valueDisplay.textContent = value;
|
||
|
|
valueDisplay.style.minWidth = '3ch';
|
||
|
|
valueDisplay.style.textAlign = 'right';
|
||
|
|
valueDisplay.style.fontVariantNumeric = 'tabular-nums';
|
||
|
|
|
||
|
|
input.addEventListener('input', (e) => {
|
||
|
|
valueDisplay.textContent = e.target.value;
|
||
|
|
if (typeof onChange === 'function') onChange(Number(e.target.value));
|
||
|
|
});
|
||
|
|
|
||
|
|
wrapper.appendChild(input);
|
||
|
|
wrapper.appendChild(valueDisplay);
|
||
|
|
container.appendChild(wrapper);
|
||
|
|
|
||
|
|
return { input, valueDisplay, getValue: () => Number(input.value) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// createRangeSlider('#slider-box', {
|
||
|
|
// min: 0, max: 255, value: 128,
|
||
|
|
// onChange: (v) => { document.body.style.background = `rgb(${v},${v},${v})`; }
|
||
|
|
// });'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build a color picker canvas with eyedropper and palette export",
|
||
|
|
'''function createColorPicker(canvasId, exportBtnId) {
|
||
|
|
const canvas = document.getElementById(canvasId);
|
||
|
|
const exportBtn = document.getElementById(exportBtnId);
|
||
|
|
if (!canvas || !exportBtn) throw new Error('Color picker elements not found');
|
||
|
|
|
||
|
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||
|
|
let isDragging = false;
|
||
|
|
const palette = new Set();
|
||
|
|
|
||
|
|
// Draw a hue-saturation gradient
|
||
|
|
function drawGradient() {
|
||
|
|
const w = canvas.width;
|
||
|
|
const h = canvas.height;
|
||
|
|
for (let x = 0; x < w; x++) {
|
||
|
|
const hue = (x / w) * 360;
|
||
|
|
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||
|
|
grad.addColorStop(0, `hsl(${hue}, 100%, 50%)`);
|
||
|
|
grad.addColorStop(1, `hsl(${hue}, 100%, 0%)`);
|
||
|
|
ctx.fillStyle = grad;
|
||
|
|
ctx.fillRect(x, 0, 1, h);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
drawGradient();
|
||
|
|
|
||
|
|
function pickColor(x, y) {
|
||
|
|
const pixel = ctx.getImageData(x, y, 1, 1).data;
|
||
|
|
const hex = '#' + [pixel[0], pixel[1], pixel[2]].map(c => c.toString(16).padStart(2, '0')).join('');
|
||
|
|
return hex;
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleMove(e) {
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
const x = Math.min(Math.max(e.clientX - rect.left, 0), canvas.width - 1);
|
||
|
|
const y = Math.min(Math.max(e.clientY - rect.top, 0), canvas.height - 1);
|
||
|
|
const color = pickColor(x, y);
|
||
|
|
canvas.style.cursor = 'crosshair';
|
||
|
|
if (isDragging) {
|
||
|
|
palette.add(color);
|
||
|
|
canvas.dispatchEvent(new CustomEvent('colorpicked', { detail: { color, x, y } }));
|
||
|
|
}
|
||
|
|
return color;
|
||
|
|
}
|
||
|
|
|
||
|
|
canvas.addEventListener('mousedown', (e) => { isDragging = true; handleMove(e); });
|
||
|
|
canvas.addEventListener('mousemove', handleMove);
|
||
|
|
canvas.addEventListener('mouseup', () => { isDragging = false; });
|
||
|
|
canvas.addEventListener('mouseleave', () => { isDragging = false; });
|
||
|
|
|
||
|
|
exportBtn.addEventListener('click', () => {
|
||
|
|
const colors = Array.from(palette);
|
||
|
|
const blob = new Blob([JSON.stringify(colors, null, 2)], { type: 'application/json' });
|
||
|
|
const url = URL.createObjectURL(blob);
|
||
|
|
const a = document.createElement('a');
|
||
|
|
a.href = url;
|
||
|
|
a.download = 'palette.json';
|
||
|
|
a.click();
|
||
|
|
URL.revokeObjectURL(url);
|
||
|
|
});
|
||
|
|
|
||
|
|
return { canvas, palette, pickColor };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createColorPicker('picker-canvas', 'export-btn');'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a live code preview playground with iframe sandbox",
|
||
|
|
'''function createCodePlayground(containerSelector) {
|
||
|
|
const container = document.querySelector(containerSelector);
|
||
|
|
if (!container) throw new Error('Playground container not found');
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="playground" style="display:flex;flex-direction:column;height:100%;font-family:monospace;">
|
||
|
|
<div style="display:flex;gap:0.5rem;padding:0.5rem;background:#f5f5f5;border-bottom:1px solid #ddd;">
|
||
|
|
<button data-lang="html">HTML</button>
|
||
|
|
<button data-lang="css">CSS</button>
|
||
|
|
<button data-lang="js">JS</button>
|
||
|
|
<button data-action="run" style="margin-left:auto;">Run</button>
|
||
|
|
</div>
|
||
|
|
<textarea class="editor" style="flex:1;resize:none;border:none;padding:1rem;background:#1e1e1e;color:#d4d4d4;font-size:14px;" spellcheck="false"></textarea>
|
||
|
|
<iframe class="preview" sandbox="allow-scripts" style="flex:1;border:none;border-top:1px solid #ddd;"></iframe>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
const editor = container.querySelector('.editor');
|
||
|
|
const preview = container.querySelector('.preview');
|
||
|
|
const files = { html: '<h1>Hello World</h1>', css: 'h1 { color: {color1}; }', js: 'console.log("ready");' };
|
||
|
|
let currentLang = 'html';
|
||
|
|
|
||
|
|
editor.value = files.html;
|
||
|
|
|
||
|
|
container.querySelectorAll('button[data-lang]').forEach(btn => {
|
||
|
|
btn.addEventListener('click', () => {
|
||
|
|
files[currentLang] = editor.value;
|
||
|
|
currentLang = btn.dataset.lang;
|
||
|
|
editor.value = files[currentLang];
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
container.querySelector('button[data-action="run"]').addEventListener('click', () => {
|
||
|
|
files[currentLang] = editor.value;
|
||
|
|
const doc = `
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head><style>${files.css}</style></head>
|
||
|
|
<body>${files.html}<script>try { ${files.js} } catch(e) { document.body.innerHTML += '<pre style=\"color:red\">' + e + '</pre>'; }</script></body>
|
||
|
|
</html>
|
||
|
|
`;
|
||
|
|
preview.srcdoc = doc;
|
||
|
|
});
|
||
|
|
|
||
|
|
return { editor, preview, getFiles: () => ({ ...files }) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createCodePlayground('#playground');'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build a resizable split-pane layout with drag handle",
|
||
|
|
'''function createSplitPane(containerSelector, options = {}) {
|
||
|
|
const container = document.querySelector(containerSelector);
|
||
|
|
if (!container) throw new Error('Split pane container not found');
|
||
|
|
|
||
|
|
const { direction = 'horizontal', initialRatio = 0.5, minSize = 100 } = options;
|
||
|
|
const isHorizontal = direction === 'horizontal';
|
||
|
|
|
||
|
|
container.style.display = 'flex';
|
||
|
|
container.style.flexDirection = isHorizontal ? 'row' : 'column';
|
||
|
|
container.style.height = '100%';
|
||
|
|
container.style.overflow = 'hidden';
|
||
|
|
|
||
|
|
const pane1 = document.createElement('div');
|
||
|
|
pane1.className = 'pane pane-1';
|
||
|
|
pane1.style.flex = `0 0 calc(${initialRatio * 100}% - 4px)`;
|
||
|
|
pane1.style.overflow = 'auto';
|
||
|
|
pane1.style.minWidth = isHorizontal ? minSize + 'px' : 'auto';
|
||
|
|
pane1.style.minHeight = !isHorizontal ? minSize + 'px' : 'auto';
|
||
|
|
|
||
|
|
const handle = document.createElement('div');
|
||
|
|
handle.className = 'split-handle';
|
||
|
|
handle.style.flex = '0 0 8px';
|
||
|
|
handle.style.background = '#e0e0e0';
|
||
|
|
handle.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
||
|
|
handle.style.userSelect = 'none';
|
||
|
|
|
||
|
|
const pane2 = document.createElement('div');
|
||
|
|
pane2.className = 'pane pane-2';
|
||
|
|
pane2.style.flex = '1 1 auto';
|
||
|
|
pane2.style.overflow = 'auto';
|
||
|
|
pane2.style.minWidth = isHorizontal ? minSize + 'px' : 'auto';
|
||
|
|
pane2.style.minHeight = !isHorizontal ? minSize + 'px' : 'auto';
|
||
|
|
|
||
|
|
container.appendChild(pane1);
|
||
|
|
container.appendChild(handle);
|
||
|
|
container.appendChild(pane2);
|
||
|
|
|
||
|
|
let isDragging = false;
|
||
|
|
const sizeProp = isHorizontal ? 'clientWidth' : 'clientHeight';
|
||
|
|
|
||
|
|
handle.addEventListener('mousedown', (e) => {
|
||
|
|
isDragging = true;
|
||
|
|
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('mousemove', (e) => {
|
||
|
|
if (!isDragging) return;
|
||
|
|
const rect = container.getBoundingClientRect();
|
||
|
|
const pos = isHorizontal ? e.clientX - rect.left : e.clientY - rect.top;
|
||
|
|
const ratio = Math.max(minSize, Math.min(pos, rect[sizeProp] - minSize)) / rect[sizeProp];
|
||
|
|
pane1.style.flex = `0 0 calc(${ratio * 100}% - 4px)`;
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('mouseup', () => {
|
||
|
|
isDragging = false;
|
||
|
|
document.body.style.cursor = '';
|
||
|
|
});
|
||
|
|
|
||
|
|
return { pane1, pane2, handle };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createSplitPane('#editor-layout', { direction: 'horizontal', initialRatio: 0.4 });'''
|
||
|
|
))
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
# ── Domain: Gallery ──
|
||
|
|
def gallery_templates():
|
||
|
|
t = []
|
||
|
|
t.append((
|
||
|
|
"Implement a masonry image grid with lazy loading and lightbox",
|
||
|
|
'''function createMasonryGallery(containerSelector, imageUrls) {
|
||
|
|
const container = document.querySelector(containerSelector);
|
||
|
|
if (!container) throw new Error('Gallery container not found');
|
||
|
|
|
||
|
|
container.style.columnCount = '3';
|
||
|
|
container.style.columnGap = '1rem';
|
||
|
|
|
||
|
|
if (!Array.isArray(imageUrls)) throw new TypeError('imageUrls must be an array');
|
||
|
|
|
||
|
|
// Lightbox overlay
|
||
|
|
const lightbox = document.createElement('div');
|
||
|
|
lightbox.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.9);display:none;align-items:center;justify-content:center;z-index:1000;';
|
||
|
|
const img = document.createElement('img');
|
||
|
|
img.style.maxWidth = '90vw';
|
||
|
|
img.style.maxHeight = '90vh';
|
||
|
|
img.style.objectFit = 'contain';
|
||
|
|
lightbox.appendChild(img);
|
||
|
|
lightbox.addEventListener('click', () => { lightbox.style.display = 'none'; });
|
||
|
|
document.body.appendChild(lightbox);
|
||
|
|
|
||
|
|
imageUrls.forEach((src, i) => {
|
||
|
|
const wrapper = document.createElement('div');
|
||
|
|
wrapper.style.breakInside = 'avoid';
|
||
|
|
wrapper.style.marginBottom = '1rem';
|
||
|
|
|
||
|
|
const image = document.createElement('img');
|
||
|
|
image.dataset.src = src;
|
||
|
|
image.alt = `Gallery image ${i + 1}`;
|
||
|
|
image.style.width = '100%';
|
||
|
|
image.style.borderRadius = '8px';
|
||
|
|
image.style.display = 'block';
|
||
|
|
image.style.background = '{color1}';
|
||
|
|
image.style.minHeight = '150px';
|
||
|
|
|
||
|
|
image.addEventListener('click', () => {
|
||
|
|
img.src = src;
|
||
|
|
lightbox.style.display = 'flex';
|
||
|
|
});
|
||
|
|
|
||
|
|
wrapper.appendChild(image);
|
||
|
|
container.appendChild(wrapper);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Lazy loading
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting && entry.target.dataset.src) {
|
||
|
|
entry.target.src = entry.target.dataset.src;
|
||
|
|
delete entry.target.dataset.src;
|
||
|
|
observer.unobserve(entry.target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, { rootMargin: '200px' });
|
||
|
|
|
||
|
|
container.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
|
||
|
|
return { container, lightbox, observer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createMasonryGallery('#gallery', ['/img/1.jpg', '/img/2.jpg']);'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create an image carousel with touch swipe and keyboard controls",
|
||
|
|
'''function createCarousel(containerSelector, slides) {
|
||
|
|
const container = document.querySelector(containerSelector);
|
||
|
|
if (!container) throw new Error('Carousel container not found');
|
||
|
|
if (!Array.isArray(slides) || slides.length === 0) throw new Error('Slides array required');
|
||
|
|
|
||
|
|
let current = 0;
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="carousel" style="position:relative;overflow:hidden;border-radius:12px;">
|
||
|
|
<div class="track" style="display:flex;transition:transform 0.4s ease;"></div>
|
||
|
|
<button class="prev" style="position:absolute;left:1rem;top:50%;transform:translateY(-50%);" aria-label="Previous slide">←</button>
|
||
|
|
<button class="next" style="position:absolute;right:1rem;top:50%;transform:translateY(-50%);" aria-label="Next slide">→</button>
|
||
|
|
<div class="indicators" style="position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);display:flex;gap:0.5rem;"></div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
const track = container.querySelector('.track');
|
||
|
|
const indicators = container.querySelector('.indicators');
|
||
|
|
|
||
|
|
slides.forEach((slide, i) => {
|
||
|
|
const div = document.createElement('div');
|
||
|
|
div.style.minWidth = '100%';
|
||
|
|
div.innerHTML = slide;
|
||
|
|
track.appendChild(div);
|
||
|
|
|
||
|
|
const dot = document.createElement('button');
|
||
|
|
dot.style.width = '10px';
|
||
|
|
dot.style.height = '10px';
|
||
|
|
dot.style.borderRadius = '50%';
|
||
|
|
dot.style.border = 'none';
|
||
|
|
dot.style.background = i === 0 ? '#fff' : 'rgba(255,255,255,0.4)';
|
||
|
|
dot.addEventListener('click', () => goTo(i));
|
||
|
|
indicators.appendChild(dot);
|
||
|
|
});
|
||
|
|
|
||
|
|
function goTo(index) {
|
||
|
|
current = ((index % slides.length) + slides.length) % slides.length;
|
||
|
|
track.style.transform = `translateX(-${current * 100}%)`;
|
||
|
|
Array.from(indicators.children).forEach((dot, i) => {
|
||
|
|
dot.style.background = i === current ? '#fff' : 'rgba(255,255,255,0.4)';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
container.querySelector('.prev').addEventListener('click', () => goTo(current - 1));
|
||
|
|
container.querySelector('.next').addEventListener('click', () => goTo(current + 1));
|
||
|
|
|
||
|
|
// Touch swipe
|
||
|
|
let startX = 0;
|
||
|
|
container.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; });
|
||
|
|
container.addEventListener('touchend', (e) => {
|
||
|
|
const diff = startX - e.changedTouches[0].clientX;
|
||
|
|
if (Math.abs(diff) > 50) goTo(current + (diff > 0 ? 1 : -1));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Keyboard
|
||
|
|
container.setAttribute('tabindex', '0');
|
||
|
|
container.addEventListener('keydown', (e) => {
|
||
|
|
if (e.key === 'ArrowLeft') goTo(current - 1);
|
||
|
|
if (e.key === 'ArrowRight') goTo(current + 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
return { goTo, getCurrent: () => current };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage: createCarousel('#carousel', ['<img src="a.jpg">', '<img src="b.jpg">']);'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build an infinite scroll image feed with skeleton placeholders",
|
||
|
|
'''function createInfiniteFeed(containerSelector, fetchPage, options = {}) {
|
||
|
|
const container = document.querySelector(containerSelector);
|
||
|
|
if (!container) throw new Error('Feed container not found');
|
||
|
|
if (typeof fetchPage !== 'function') throw new TypeError('fetchPage must be a function');
|
||
|
|
|
||
|
|
const { pageSize = 20, threshold = 300 } = options;
|
||
|
|
let page = 1;
|
||
|
|
let isLoading = false;
|
||
|
|
let hasMore = true;
|
||
|
|
|
||
|
|
function createSkeletons(count) {
|
||
|
|
const frag = document.createDocumentFragment();
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const div = document.createElement('div');
|
||
|
|
div.className = 'skeleton';
|
||
|
|
div.style.cssText = 'height:200px;background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:8px;';
|
||
|
|
frag.appendChild(div);
|
||
|
|
}
|
||
|
|
return frag;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add shimmer keyframes if not present
|
||
|
|
if (!document.getElementById('skeleton-styles')) {
|
||
|
|
const style = document.createElement('style');
|
||
|
|
style.id = 'skeleton-styles';
|
||
|
|
style.textContent = '@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }';
|
||
|
|
document.head.appendChild(style);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadMore() {
|
||
|
|
if (isLoading || !hasMore) return;
|
||
|
|
isLoading = true;
|
||
|
|
const skeletons = createSkeletons(pageSize);
|
||
|
|
container.appendChild(skeletons);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const items = await fetchPage(page, pageSize);
|
||
|
|
skeletons.remove();
|
||
|
|
if (!items || items.length === 0) {
|
||
|
|
hasMore = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
items.forEach(item => container.appendChild(item));
|
||
|
|
page++;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Feed load error:', err);
|
||
|
|
skeletons.remove();
|
||
|
|
} finally {
|
||
|
|
isLoading = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const sentinel = document.createElement('div');
|
||
|
|
sentinel.style.height = '1px';
|
||
|
|
container.appendChild(sentinel);
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
if (entries[0].isIntersecting) loadMore();
|
||
|
|
}, { rootMargin: `${threshold}px` });
|
||
|
|
observer.observe(sentinel);
|
||
|
|
|
||
|
|
loadMore();
|
||
|
|
return { loadMore, observer };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// createInfiniteFeed('#feed', async (page, size) => {
|
||
|
|
// const res = await fetch(`/api/images?page=${page}&size=${size}`);
|
||
|
|
// const data = await res.json();
|
||
|
|
// return data.map(url => { const img = document.createElement('img'); img.src = url; return img; });
|
||
|
|
// });'''
|
||
|
|
))
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
# ── Domain: Games ──
|
||
|
|
def game_templates():
|
||
|
|
t = []
|
||
|
|
t.append((
|
||
|
|
"Create a 2D canvas game loop with delta-time physics",
|
||
|
|
'''class GameEngine {
|
||
|
|
constructor(canvasId, options = {}) {
|
||
|
|
this.canvas = document.getElementById(canvasId);
|
||
|
|
if (!this.canvas) throw new Error('Canvas not found: ' + canvasId);
|
||
|
|
this.ctx = this.canvas.getContext('2d');
|
||
|
|
this.entities = [];
|
||
|
|
this.lastTime = 0;
|
||
|
|
this.running = false;
|
||
|
|
this.fps = 60;
|
||
|
|
|
||
|
|
this.canvas.width = options.width || 800;
|
||
|
|
this.canvas.height = options.height || 600;
|
||
|
|
this.canvas.style.background = options.bg || '{color1}';
|
||
|
|
}
|
||
|
|
|
||
|
|
addEntity(entity) {
|
||
|
|
if (!entity.update || !entity.draw) {
|
||
|
|
throw new TypeError('Entity must have update(dt) and draw(ctx) methods');
|
||
|
|
}
|
||
|
|
this.entities.push(entity);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
start() {
|
||
|
|
this.running = true;
|
||
|
|
requestAnimationFrame((t) => this.loop(t));
|
||
|
|
}
|
||
|
|
|
||
|
|
stop() {
|
||
|
|
this.running = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
loop(timestamp) {
|
||
|
|
if (!this.running) return;
|
||
|
|
const dt = Math.min((timestamp - this.lastTime) / 1000, 0.05); // Cap delta
|
||
|
|
this.lastTime = timestamp;
|
||
|
|
|
||
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
|
|
|
||
|
|
for (const entity of this.entities) {
|
||
|
|
entity.update(dt, this.canvas.width, this.canvas.height);
|
||
|
|
entity.draw(this.ctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
requestAnimationFrame((t) => this.loop(t));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// const engine = new GameEngine('game-canvas', { width: 800, height: 600 });
|
||
|
|
// engine.addEntity({ update(dt, w, h) { this.x += 100 * dt; }, draw(ctx) { ctx.fillRect(this.x, 100, 20, 20); }, x: 0 });
|
||
|
|
// engine.start();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Implement AABB collision detection for rectangular game entities",
|
||
|
|
'''function checkAABBCollision(a, b) {
|
||
|
|
if (!a || !b) throw new Error('Both entities required for collision check');
|
||
|
|
return (
|
||
|
|
a.x < b.x + b.width &&
|
||
|
|
a.x + a.width > b.x &&
|
||
|
|
a.y < b.y + b.height &&
|
||
|
|
a.y + a.height > b.y
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveAABBOverlap(a, b) {
|
||
|
|
const overlapX = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
||
|
|
const overlapY = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
||
|
|
|
||
|
|
if (overlapX < overlapY) {
|
||
|
|
const dir = a.x < b.x ? -1 : 1;
|
||
|
|
a.x += (overlapX / 2) * dir;
|
||
|
|
b.x -= (overlapX / 2) * dir;
|
||
|
|
} else {
|
||
|
|
const dir = a.y < b.y ? -1 : 1;
|
||
|
|
a.y += (overlapY / 2) * dir;
|
||
|
|
b.y -= (overlapY / 2) * dir;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class PhysicsWorld {
|
||
|
|
constructor() {
|
||
|
|
this.bodies = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
add(body) {
|
||
|
|
if (typeof body.x !== 'number' || typeof body.y !== 'number') {
|
||
|
|
throw new TypeError('Body must have numeric x and y properties');
|
||
|
|
}
|
||
|
|
this.bodies.push(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
step() {
|
||
|
|
for (let i = 0; i < this.bodies.length; i++) {
|
||
|
|
for (let j = i + 1; j < this.bodies.length; j++) {
|
||
|
|
if (checkAABBCollision(this.bodies[i], this.bodies[j])) {
|
||
|
|
resolveAABBOverlap(this.bodies[i], this.bodies[j]);
|
||
|
|
if (this.bodies[i].onCollision) this.bodies[i].onCollision(this.bodies[j]);
|
||
|
|
if (this.bodies[j].onCollision) this.bodies[j].onCollision(this.bodies[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// const world = new PhysicsWorld();
|
||
|
|
// world.add({ x: 0, y: 0, width: 32, height: 32, onCollision(other) { console.log('hit!'); } });
|
||
|
|
// world.step();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Build a sprite animation system with frame clipping and playback controls",
|
||
|
|
'''class SpriteAnimator {
|
||
|
|
constructor(image, frameWidth, frameHeight, frameCount) {
|
||
|
|
if (!(image instanceof HTMLImageElement)) throw new TypeError('Expected HTMLImageElement');
|
||
|
|
this.image = image;
|
||
|
|
this.frameWidth = frameWidth;
|
||
|
|
this.frameHeight = frameHeight;
|
||
|
|
this.frameCount = frameCount;
|
||
|
|
this.currentFrame = 0;
|
||
|
|
this.elapsed = 0;
|
||
|
|
this.fps = 10;
|
||
|
|
this.playing = true;
|
||
|
|
this.loop = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
update(dt) {
|
||
|
|
if (!this.playing) return;
|
||
|
|
this.elapsed += dt;
|
||
|
|
const frameDuration = 1 / this.fps;
|
||
|
|
if (this.elapsed >= frameDuration) {
|
||
|
|
this.elapsed -= frameDuration;
|
||
|
|
this.currentFrame++;
|
||
|
|
if (this.currentFrame >= this.frameCount) {
|
||
|
|
if (this.loop) this.currentFrame = 0;
|
||
|
|
else { this.currentFrame = this.frameCount - 1; this.playing = false; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
draw(ctx, x, y, options = {}) {
|
||
|
|
const sx = (this.currentFrame * this.frameWidth) % this.image.width;
|
||
|
|
const sy = Math.floor((this.currentFrame * this.frameWidth) / this.image.width) * this.frameHeight;
|
||
|
|
const scale = options.scale || 1;
|
||
|
|
ctx.drawImage(
|
||
|
|
this.image,
|
||
|
|
sx, sy, this.frameWidth, this.frameHeight,
|
||
|
|
x, y, this.frameWidth * scale, this.frameHeight * scale
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
play() { this.playing = true; }
|
||
|
|
pause() { this.playing = false; }
|
||
|
|
reset() { this.currentFrame = 0; this.elapsed = 0; }
|
||
|
|
setFrame(index) { this.currentFrame = Math.max(0, Math.min(index, this.frameCount - 1)); }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// const img = new Image();
|
||
|
|
// img.src = '/sprites/player.png';
|
||
|
|
// img.onload = () => {
|
||
|
|
// const anim = new SpriteAnimator(img, 32, 32, 8);
|
||
|
|
// // In game loop: anim.update(dt); anim.draw(ctx, 100, 100);
|
||
|
|
// };'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Implement a tilemap renderer with camera scrolling and culling",
|
||
|
|
'''class TilemapRenderer {
|
||
|
|
constructor(canvasId, tileSize = 32) {
|
||
|
|
this.canvas = document.getElementById(canvasId);
|
||
|
|
if (!this.canvas) throw new Error('Canvas not found');
|
||
|
|
this.ctx = this.canvas.getContext('2d');
|
||
|
|
this.tileSize = tileSize;
|
||
|
|
this.camera = { x: 0, y: 0 };
|
||
|
|
this.tiles = []; // 2D array of tile IDs
|
||
|
|
this.tileset = new Map(); // ID -> color or image
|
||
|
|
}
|
||
|
|
|
||
|
|
loadMap(tiles) {
|
||
|
|
if (!Array.isArray(tiles) || !tiles.every(row => Array.isArray(row))) {
|
||
|
|
throw new TypeError('tiles must be a 2D array');
|
||
|
|
}
|
||
|
|
this.tiles = tiles;
|
||
|
|
}
|
||
|
|
|
||
|
|
registerTile(id, renderable) {
|
||
|
|
this.tileset.set(id, renderable);
|
||
|
|
}
|
||
|
|
|
||
|
|
setCamera(x, y) {
|
||
|
|
this.camera.x = x;
|
||
|
|
this.camera.y = y;
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
const cols = Math.ceil(this.canvas.width / this.tileSize) + 1;
|
||
|
|
const rows = Math.ceil(this.canvas.height / this.tileSize) + 1;
|
||
|
|
const startCol = Math.floor(this.camera.x / this.tileSize);
|
||
|
|
const startRow = Math.floor(this.camera.y / this.tileSize);
|
||
|
|
|
||
|
|
for (let r = 0; r < rows; r++) {
|
||
|
|
for (let c = 0; c < cols; c++) {
|
||
|
|
const tileRow = startRow + r;
|
||
|
|
const tileCol = startCol + c;
|
||
|
|
if (tileRow < 0 || tileRow >= this.tiles.length) continue;
|
||
|
|
if (tileCol < 0 || tileCol >= this.tiles[tileRow].length) continue;
|
||
|
|
|
||
|
|
const tileId = this.tiles[tileRow][tileCol];
|
||
|
|
const screenX = c * this.tileSize - (this.camera.x % this.tileSize);
|
||
|
|
const screenY = r * this.tileSize - (this.camera.y % this.tileSize);
|
||
|
|
|
||
|
|
const renderable = this.tileset.get(tileId);
|
||
|
|
if (typeof renderable === 'string') {
|
||
|
|
this.ctx.fillStyle = renderable;
|
||
|
|
this.ctx.fillRect(screenX, screenY, this.tileSize, this.tileSize);
|
||
|
|
} else if (renderable instanceof HTMLImageElement) {
|
||
|
|
this.ctx.drawImage(renderable, screenX, screenY, this.tileSize, this.tileSize);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// const renderer = new TilemapRenderer('game-canvas', 32);
|
||
|
|
// renderer.loadMap([[0,0,1],[0,1,1],[1,1,1]]);
|
||
|
|
// renderer.registerTile(0, '{color1}');
|
||
|
|
// renderer.registerTile(1, '{color2}');
|
||
|
|
// renderer.setCamera(100, 50);
|
||
|
|
// renderer.render();'''
|
||
|
|
))
|
||
|
|
t.append((
|
||
|
|
"Create a particle explosion effect on canvas for game feedback",
|
||
|
|
'''class ParticleSystem {
|
||
|
|
constructor() {
|
||
|
|
this.particles = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
emit(x, y, options = {}) {
|
||
|
|
const {
|
||
|
|
count = {count},
|
||
|
|
speed = {speed},
|
||
|
|
life = 1.0,
|
||
|
|
colors = {colors},
|
||
|
|
size = 4,
|
||
|
|
gravity = 200
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
|
||
|
|
const velocity = speed * (0.5 + Math.random() * 0.5);
|
||
|
|
this.particles.push({
|
||
|
|
x, y,
|
||
|
|
vx: Math.cos(angle) * velocity,
|
||
|
|
vy: Math.sin(angle) * velocity,
|
||
|
|
life,
|
||
|
|
maxLife: life,
|
||
|
|
color: colors[Math.floor(Math.random() * colors.length)],
|
||
|
|
size: size * (0.5 + Math.random()),
|
||
|
|
gravity
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
update(dt) {
|
||
|
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||
|
|
const p = this.particles[i];
|
||
|
|
p.x += p.vx * dt;
|
||
|
|
p.y += p.vy * dt;
|
||
|
|
p.vy += p.gravity * dt;
|
||
|
|
p.life -= dt;
|
||
|
|
if (p.life <= 0) this.particles.splice(i, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
draw(ctx) {
|
||
|
|
for (const p of this.particles) {
|
||
|
|
const alpha = Math.max(0, p.life / p.maxLife);
|
||
|
|
ctx.globalAlpha = alpha;
|
||
|
|
ctx.fillStyle = p.color;
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
|
||
|
|
ctx.fill();
|
||
|
|
}
|
||
|
|
ctx.globalAlpha = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage:
|
||
|
|
// const particles = new ParticleSystem();
|
||
|
|
// particles.emit(400, 300, { count: 30, speed: 150, colors: ['{color1}', '{color2}', '{color3}'] });
|
||
|
|
// // In game loop: particles.update(dt); particles.draw(ctx);'''
|
||
|
|
))
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
# ── Generator ──
|
||
|
|
def generate(count=1000):
|
||
|
|
rng = random.Random(42)
|
||
|
|
templates = (
|
||
|
|
threejs_templates() +
|
||
|
|
htmlcss_templates() +
|
||
|
|
playground_templates() +
|
||
|
|
gallery_templates() +
|
||
|
|
game_templates()
|
||
|
|
)
|
||
|
|
|
||
|
|
entries = []
|
||
|
|
tpl_idx = 0
|
||
|
|
variant = 0
|
||
|
|
|
||
|
|
while len(entries) < count:
|
||
|
|
problem_template, solution_template = templates[tpl_idx % len(templates)]
|
||
|
|
tpl_idx += 1
|
||
|
|
|
||
|
|
color1 = pick(COLORS, rng)
|
||
|
|
color2 = pick(COLORS, rng)
|
||
|
|
color3 = pick(COLORS, rng)
|
||
|
|
name = pick(NAMES, rng)
|
||
|
|
adj = pick(ADJECTIVES, rng)
|
||
|
|
size = pick(SIZES, rng)
|
||
|
|
speed = pick(SPEEDS, rng)
|
||
|
|
cnt = pick(COUNTS, rng)
|
||
|
|
|
||
|
|
problem = problem_template.replace('{name}', name).replace('{adj}', adj)
|
||
|
|
solution = (solution_template
|
||
|
|
.replace('{color1}', color1)
|
||
|
|
.replace('{color2}', color2)
|
||
|
|
.replace('{color3}', color3)
|
||
|
|
.replace('{name}', name)
|
||
|
|
.replace('{adj}', adj)
|
||
|
|
.replace('{size}', size)
|
||
|
|
.replace('{speed}', str(speed))
|
||
|
|
.replace('{count}', str(cnt))
|
||
|
|
.replace('{colors}', str(picks(COLORS, 3, rng))))
|
||
|
|
|
||
|
|
# Determine domain from template source
|
||
|
|
domain = 'threejs'
|
||
|
|
lang = 'javascript'
|
||
|
|
tags = ['frontend']
|
||
|
|
if problem_template in [p for p, _ in htmlcss_templates()]:
|
||
|
|
domain = 'html-css-js'
|
||
|
|
lang = 'css' if problem_template.startswith('Build a responsive CSS') or 'grid' in problem_template else 'javascript'
|
||
|
|
tags = ['frontend', 'css', 'dom']
|
||
|
|
elif problem_template in [p for p, _ in playground_templates()]:
|
||
|
|
domain = 'playground'
|
||
|
|
tags = ['frontend', 'ui', 'interactive']
|
||
|
|
elif problem_template in [p for p, _ in gallery_templates()]:
|
||
|
|
domain = 'gallery'
|
||
|
|
tags = ['frontend', 'images', 'performance']
|
||
|
|
elif problem_template in [p for p, _ in game_templates()]:
|
||
|
|
domain = 'games'
|
||
|
|
tags = ['frontend', 'canvas', 'game-dev']
|
||
|
|
|
||
|
|
if 'three.js' in problem.lower():
|
||
|
|
domain = 'threejs'
|
||
|
|
tags = ['frontend', 'threejs', 'webgl']
|
||
|
|
|
||
|
|
entries.append({
|
||
|
|
'problem': problem,
|
||
|
|
'solution': solution,
|
||
|
|
'domain': domain,
|
||
|
|
'language': lang,
|
||
|
|
'tags': tags,
|
||
|
|
'variant': variant
|
||
|
|
})
|
||
|
|
variant += 1
|
||
|
|
|
||
|
|
return entries
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description='Generate frontend code pattern training pairs')
|
||
|
|
parser.add_argument('--count', type=int, default=1000, help='Number of pairs to generate')
|
||
|
|
parser.add_argument('--output', type=str, default='training-data/code-patterns-frontend-&-creative.jsonl')
|
||
|
|
parser.add_argument('--seed', type=int, default=42, help='Random seed')
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
random.seed(args.seed)
|
||
|
|
entries = generate(args.count)
|
||
|
|
|
||
|
|
out_path = Path(args.output)
|
||
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
with open(out_path, 'w', encoding='utf-8') as f:
|
||
|
|
for entry in entries:
|
||
|
|
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
||
|
|
|
||
|
|
print(f'Generated {len(entries)} code pattern pairs → {out_path}')
|
||
|
|
print(f'Size: {out_path.stat().st_size / 1024:.1f} KB')
|
||
|
|
|
||
|
|
# Print domain distribution
|
||
|
|
from collections import Counter
|
||
|
|
dist = Counter(e['domain'] for e in entries)
|
||
|
|
print('Domain distribution:')
|
||
|
|
for d, c in sorted(dist.items()):
|
||
|
|
print(f' {d}: {c}')
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|