@pixel-normal-edit/mcp 2.0.6 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/domains/art-tree/3d_lessons.js +114 -0
- package/domains/art-tree/3d_shapes.js +116 -0
- package/domains/art-tree/3d_tools.js +60 -0
- package/domains/art-tree/advanced_lessons.js +124 -0
- package/domains/art-tree/advanced_shapes.js +73 -0
- package/domains/art-tree/advanced_tools.js +69 -0
- package/domains/art-tree/analysis_lessons.js +32 -0
- package/domains/art-tree/analysis_shapes.js +146 -0
- package/domains/art-tree/analysis_tools.js +49 -0
- package/domains/art-tree/cross_section_lessons.js +65 -0
- package/domains/art-tree/cross_section_shapes.js +84 -0
- package/domains/art-tree/cross_section_tools.js +51 -0
- package/domains/art-tree/curve_lessons.js +138 -0
- package/domains/art-tree/curve_shapes.js +90 -0
- package/domains/art-tree/curve_tools.js +51 -0
- package/domains/art-tree/ellipse_lessons.js +105 -0
- package/domains/art-tree/ellipse_shapes.js +63 -0
- package/domains/art-tree/ellipse_tools.js +60 -0
- package/domains/art-tree/hidden_lessons.js +55 -0
- package/domains/art-tree/hidden_shapes.js +97 -0
- package/domains/art-tree/hidden_tools.js +51 -0
- package/domains/art-tree/index.js +32 -0
- package/domains/art-tree/layer_lessons.js +32 -0
- package/domains/art-tree/layer_shapes.js +117 -0
- package/domains/art-tree/layer_tools.js +43 -0
- package/domains/art-tree/lessons.js +119 -7
- package/domains/art-tree/light_lessons.js +92 -0
- package/domains/art-tree/light_shapes.js +136 -0
- package/domains/art-tree/light_tools.js +49 -0
- package/domains/art-tree/material_lessons.js +71 -0
- package/domains/art-tree/material_shapes.js +130 -0
- package/domains/art-tree/material_tools.js +49 -0
- package/domains/art-tree/perspective_lessons.js +138 -0
- package/domains/art-tree/perspective_shapes.js +96 -0
- package/domains/art-tree/perspective_tools.js +60 -0
- package/domains/art-tree/shapes.js +15 -0
- package/domains/art-tree/sky_lessons_1.js +82 -0
- package/domains/art-tree/sky_shapes_1.js +68 -0
- package/domains/art-tree/sky_tools.js +51 -0
- package/domains/art-tree/structure_lessons.js +102 -0
- package/domains/art-tree/structure_shapes.js +74 -0
- package/domains/art-tree/structure_tools.js +51 -0
- package/domains/art-tree/surface_lessons.js +112 -0
- package/domains/art-tree/surface_shapes.js +87 -0
- package/domains/art-tree/surface_tools.js +60 -0
- package/domains/art-tree/tools.js +69 -15
- package/domains/art-tree/transform_lessons_1.js +108 -0
- package/domains/art-tree/transform_lessons_2.js +102 -0
- package/domains/art-tree/transform_shapes.js +109 -0
- package/domains/art-tree/transform_tools.js +104 -0
- package/domains/art-tree/vocab_lessons.js +32 -0
- package/domains/art-tree/vocab_shapes.js +116 -0
- package/domains/art-tree/vocab_tools.js +41 -0
- package/package.json +1 -1
- package/tools/index.js +2 -0
- package/tools/layer.js +80 -0
|
@@ -165,19 +165,125 @@ function lessonComposition(canvasSize = 32) {
|
|
|
165
165
|
];
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Lesson 8: Freehand — Practice freehand drawing with simulated hand tremor.
|
|
170
|
+
*/
|
|
171
|
+
function lessonFreehand(canvasSize = 32, color = '#795548') {
|
|
172
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
173
|
+
const points = [];
|
|
174
|
+
let y = Math.floor(canvasSize / 2);
|
|
175
|
+
for (let x = margin; x < canvasSize - margin; x += 2) {
|
|
176
|
+
points.push({ x, y: y + Math.floor(Math.random() * 3) - 1 });
|
|
177
|
+
}
|
|
178
|
+
return shapes.drawPolyline(points, color);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Lesson 9: Proportions and Angles — Practice maintaining proportions and angles.
|
|
183
|
+
*/
|
|
184
|
+
function lessonProportionsAndAngles(canvasSize = 32, color = '#3f51b5') {
|
|
185
|
+
const mid = Math.floor(canvasSize / 2);
|
|
186
|
+
const size1 = Math.floor(canvasSize * 0.4);
|
|
187
|
+
const size2 = Math.floor(size1 / 2);
|
|
188
|
+
return [
|
|
189
|
+
shapes.drawSquare(mid - Math.floor(size1/2), mid - Math.floor(size1/2), size1, color, false),
|
|
190
|
+
shapes.drawSquare(mid - Math.floor(size2/2), mid - Math.floor(size2/2), size2, '#e91e63', false),
|
|
191
|
+
shapes.drawLine(mid, mid, mid + size1, mid - size1, '#009688')
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Lesson 10: Curves — C-curves and S-curves.
|
|
197
|
+
*/
|
|
198
|
+
function lessonCurves(canvasSize = 32, color = '#9c27b0') {
|
|
199
|
+
const commands = [];
|
|
200
|
+
const mid = Math.floor(canvasSize / 2);
|
|
201
|
+
const r = Math.floor(canvasSize * 0.15);
|
|
202
|
+
// C curve
|
|
203
|
+
const cPoints = [];
|
|
204
|
+
for (let i = -Math.PI/2; i <= Math.PI/2; i += 0.2) {
|
|
205
|
+
cPoints.push({ x: Math.round(mid - r - r * Math.cos(i)), y: Math.round(mid / 2 + r * Math.sin(i)) });
|
|
206
|
+
}
|
|
207
|
+
commands.push(...shapes.drawPolyline(cPoints, color));
|
|
208
|
+
// S curve
|
|
209
|
+
const sPoints = [];
|
|
210
|
+
for (let i = -Math.PI/2; i <= Math.PI*1.5; i += 0.2) {
|
|
211
|
+
const isTop = i < Math.PI/2;
|
|
212
|
+
const cy = isTop ? (mid + Math.floor(canvasSize*0.1)) : (mid + Math.floor(canvasSize*0.1) + r*2);
|
|
213
|
+
const signX = isTop ? -1 : 1;
|
|
214
|
+
sPoints.push({ x: Math.round(mid + r + signX * r * Math.cos(i)), y: Math.round(cy + r * Math.sin(i)) });
|
|
215
|
+
}
|
|
216
|
+
commands.push(...shapes.drawPolyline(sPoints, '#e91e63'));
|
|
217
|
+
return commands;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Lesson 11: Spiral — Drawing spiral curves.
|
|
222
|
+
*/
|
|
223
|
+
function lessonSpiral(canvasSize = 32, color = '#ff9800') {
|
|
224
|
+
const mid = Math.floor(canvasSize / 2);
|
|
225
|
+
const points = [];
|
|
226
|
+
const maxRadius = Math.floor(canvasSize * 0.4);
|
|
227
|
+
const loops = 3;
|
|
228
|
+
for (let angle = 0; angle < Math.PI * 2 * loops; angle += 0.3) {
|
|
229
|
+
const r = (angle / (Math.PI * 2 * loops)) * maxRadius;
|
|
230
|
+
points.push({ x: Math.round(mid + r * Math.cos(angle)), y: Math.round(mid + r * Math.sin(angle)) });
|
|
231
|
+
}
|
|
232
|
+
return shapes.drawPolyline(points, color);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Lesson 12: Strokes — Long strokes, short strokes, thick and thin lines.
|
|
237
|
+
*/
|
|
238
|
+
function lessonStrokes(canvasSize = 32, color = '#2196f3') {
|
|
239
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
240
|
+
const y1 = Math.floor(canvasSize * 0.3);
|
|
241
|
+
const y2 = Math.floor(canvasSize * 0.5);
|
|
242
|
+
const y3 = Math.floor(canvasSize * 0.7);
|
|
243
|
+
return [
|
|
244
|
+
shapes.drawLine(margin, y1, canvasSize - margin, y1, color),
|
|
245
|
+
shapes.drawLine(margin, y2, Math.floor(canvasSize / 2), y2, '#4caf50'),
|
|
246
|
+
shapes.drawLine(margin, y3, canvasSize - margin, y3, '#f44336'),
|
|
247
|
+
shapes.drawLine(margin, y3 + 1, canvasSize - margin, y3 + 1, '#f44336'),
|
|
248
|
+
shapes.drawLine(margin, y3 + 2, canvasSize - margin, y3 + 2, '#f44336'),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Lesson 13: Parallel & Intersecting Lines — Drawing parallel and intersecting lines.
|
|
254
|
+
*/
|
|
255
|
+
function lessonParallelAndIntersecting(canvasSize = 32, color = '#00bcd4') {
|
|
256
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
257
|
+
const mid = Math.floor(canvasSize / 2);
|
|
258
|
+
const offset = 4;
|
|
259
|
+
return [
|
|
260
|
+
shapes.drawLine(margin, margin, canvasSize - margin, margin, color),
|
|
261
|
+
shapes.drawLine(margin, margin + offset, canvasSize - margin, margin + offset, color),
|
|
262
|
+
shapes.drawLine(margin, margin + offset * 2, canvasSize - margin, margin + offset * 2, color),
|
|
263
|
+
shapes.drawLine(mid - offset*2, mid, mid + offset*2, canvasSize - margin, '#ff5722'),
|
|
264
|
+
shapes.drawLine(mid + offset*2, mid, mid - offset*2, canvasSize - margin, '#ff5722')
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
168
268
|
/**
|
|
169
269
|
* Get all available lessons with metadata.
|
|
170
270
|
* @returns {Array<{id: string, name: string, description: string, fn: Function}>}
|
|
171
271
|
*/
|
|
172
272
|
function getLessonCatalog() {
|
|
173
273
|
return [
|
|
174
|
-
{ id: 'lines', name: '
|
|
175
|
-
{ id: 'squares', name: '
|
|
176
|
-
{ id: 'circles', name: '
|
|
177
|
-
{ id: 'ellipses', name: '
|
|
178
|
-
{ id: 'triangles', name: '
|
|
179
|
-
{ id: 'polygons', name: '
|
|
180
|
-
{ id: 'composition', name: '
|
|
274
|
+
{ id: 'lines', name: 'Lines', description: 'Draw horizontal, vertical and diagonal lines', fn: lessonLines },
|
|
275
|
+
{ id: 'squares', name: 'Squares & Rectangles', description: 'Understand right angles and proportions', fn: lessonSquares },
|
|
276
|
+
{ id: 'circles', name: 'Circles', description: 'Learn curves and symmetry', fn: lessonCircles },
|
|
277
|
+
{ id: 'ellipses', name: 'Ellipses', description: 'Understand oval shapes and proportions', fn: lessonEllipses },
|
|
278
|
+
{ id: 'triangles', name: 'Triangles', description: 'Three-point shapes and stability', fn: lessonTriangles },
|
|
279
|
+
{ id: 'polygons', name: 'Polygons', description: 'Multi-sided shapes', fn: lessonPolygons },
|
|
280
|
+
{ id: 'composition', name: 'Composition', description: 'Combine basic shapes to create simple objects', fn: lessonComposition },
|
|
281
|
+
{ id: 'freehand', name: 'Freehand Drawing', description: 'Practice freehand drawing without rulers', fn: lessonFreehand },
|
|
282
|
+
{ id: 'proportions', name: 'Proportions & Angles', description: 'Practice maintaining proportions and angles', fn: lessonProportionsAndAngles },
|
|
283
|
+
{ id: 'curves', name: 'Curves', description: 'C-curves and S-curves', fn: lessonCurves },
|
|
284
|
+
{ id: 'spiral', name: 'Spiral', description: 'Drawing spiral curves', fn: lessonSpiral },
|
|
285
|
+
{ id: 'strokes', name: 'Stroke Types', description: 'Long strokes, short strokes, thick and thin lines', fn: lessonStrokes },
|
|
286
|
+
{ id: 'parallel_intersecting', name: 'Parallel & Intersecting', description: 'Draw parallel and intersecting lines', fn: lessonParallelAndIntersecting },
|
|
181
287
|
];
|
|
182
288
|
}
|
|
183
289
|
|
|
@@ -189,5 +295,11 @@ module.exports = {
|
|
|
189
295
|
lessonTriangles,
|
|
190
296
|
lessonPolygons,
|
|
191
297
|
lessonComposition,
|
|
298
|
+
lessonFreehand,
|
|
299
|
+
lessonProportionsAndAngles,
|
|
300
|
+
lessonCurves,
|
|
301
|
+
lessonSpiral,
|
|
302
|
+
lessonStrokes,
|
|
303
|
+
lessonParallelAndIntersecting,
|
|
192
304
|
getLessonCatalog,
|
|
193
305
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const lightShapes = require('./light_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson: 6 Zones of Light (Sphere)
|
|
6
|
+
* Highlight, Light, Halftone, Core shadow, Reflected light, Cast shadow.
|
|
7
|
+
*/
|
|
8
|
+
function lessonLightZones(canvasSize = 32) {
|
|
9
|
+
const commands = [];
|
|
10
|
+
const cx = Math.floor(canvasSize / 2);
|
|
11
|
+
const cy = Math.floor(canvasSize / 2);
|
|
12
|
+
const r = Math.floor(canvasSize * 0.25);
|
|
13
|
+
|
|
14
|
+
// Light from top-left (approx -45 degrees or -135 degrees mathematically)
|
|
15
|
+
const lightAngle = -Math.PI * 0.75;
|
|
16
|
+
|
|
17
|
+
// Draw light source
|
|
18
|
+
const lx = cx + Math.floor(r * 2 * Math.cos(lightAngle));
|
|
19
|
+
const ly = cy + Math.floor(r * 2 * Math.sin(lightAngle));
|
|
20
|
+
commands.push(...lightShapes.drawLightSource(lx, ly, Math.floor(canvasSize * 0.05)));
|
|
21
|
+
|
|
22
|
+
// Draw shaded sphere
|
|
23
|
+
commands.push(...lightShapes.drawShadedSphere(cx, cy, r, lightAngle));
|
|
24
|
+
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lesson: Light Direction & Intensity (Light direction and plane values)
|
|
30
|
+
* Shows a box with planes facing/turning away from light.
|
|
31
|
+
*/
|
|
32
|
+
function lessonLightDirection(canvasSize = 32) {
|
|
33
|
+
const commands = [];
|
|
34
|
+
const cx = Math.floor(canvasSize / 2);
|
|
35
|
+
const cy = Math.floor(canvasSize / 2);
|
|
36
|
+
const w = Math.floor(canvasSize * 0.25);
|
|
37
|
+
const h = Math.floor(canvasSize * 0.25);
|
|
38
|
+
|
|
39
|
+
// Light from top-left
|
|
40
|
+
const lightAngle = -Math.PI * 0.8;
|
|
41
|
+
|
|
42
|
+
const lx = cx - Math.floor(canvasSize * 0.35);
|
|
43
|
+
const ly = cy - Math.floor(canvasSize * 0.35);
|
|
44
|
+
commands.push(...lightShapes.drawLightSource(lx, ly, Math.floor(canvasSize * 0.05)));
|
|
45
|
+
|
|
46
|
+
// Draw shaded box
|
|
47
|
+
commands.push(...lightShapes.drawShadedBox(cx, cy, w, h, lightAngle));
|
|
48
|
+
|
|
49
|
+
return commands;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Lesson: Contact Shadow (Point of contact shadow)
|
|
54
|
+
* The darkest area where an object touches the ground.
|
|
55
|
+
*/
|
|
56
|
+
function lessonContactShadow(canvasSize = 32) {
|
|
57
|
+
const commands = [];
|
|
58
|
+
const cx = Math.floor(canvasSize / 2);
|
|
59
|
+
const cy = Math.floor(canvasSize / 2);
|
|
60
|
+
const r = Math.floor(canvasSize * 0.3);
|
|
61
|
+
|
|
62
|
+
// Ground line
|
|
63
|
+
commands.push(shapes.drawLine(0, cy + r, canvasSize, cy + r, '#9e9e9e'));
|
|
64
|
+
|
|
65
|
+
// Sphere outline
|
|
66
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#bdbdbd', false));
|
|
67
|
+
|
|
68
|
+
// Cast shadow (faint)
|
|
69
|
+
commands.push(shapes.drawEllipse(cx + Math.floor(r*0.5), cy + r, Math.floor(r*1.2), Math.floor(r*0.2), '#757575', true));
|
|
70
|
+
|
|
71
|
+
// CONTACT SHADOW (Pitch black, right at the touching point)
|
|
72
|
+
// Draw a very small, very dark ellipse directly under the center
|
|
73
|
+
commands.push(shapes.drawEllipse(cx, cy + r, Math.floor(r * 0.4), 2, '#000000', true));
|
|
74
|
+
commands.push(shapes.drawEllipse(cx, cy + r, Math.floor(r * 0.2), 1, '#000000', true));
|
|
75
|
+
|
|
76
|
+
return commands;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getLightLessonCatalog() {
|
|
80
|
+
return [
|
|
81
|
+
{ id: 'light_zones', name: '6 Zones of Light', description: 'Highlight to Cast shadow on a sphere', fn: lessonLightZones },
|
|
82
|
+
{ id: 'light_dir', name: 'Light Direction', description: 'Planes facing vs turning away from light', fn: lessonLightDirection },
|
|
83
|
+
{ id: 'light_contact', name: 'Contact Shadow', description: 'The darkest point where object touches ground', fn: lessonContactShadow },
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
lessonLightZones,
|
|
89
|
+
lessonLightDirection,
|
|
90
|
+
lessonContactShadow,
|
|
91
|
+
getLightLessonCatalog
|
|
92
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
function drawLightSource(cx, cy, r) {
|
|
4
|
+
const commands = [];
|
|
5
|
+
const color = '#ffeb3b'; // Yellow sun
|
|
6
|
+
commands.push(shapes.drawCircle(cx, cy, r, color, true));
|
|
7
|
+
|
|
8
|
+
// Rays
|
|
9
|
+
const rayL = Math.floor(r * 1.5);
|
|
10
|
+
for (let i = 0; i < 8; i++) {
|
|
11
|
+
const a = (i / 8) * 2 * Math.PI;
|
|
12
|
+
const x1 = Math.round(cx + (r + 2) * Math.cos(a));
|
|
13
|
+
const y1 = Math.round(cy + (r + 2) * Math.sin(a));
|
|
14
|
+
const x2 = Math.round(cx + (r + rayL) * Math.cos(a));
|
|
15
|
+
const y2 = Math.round(cy + (r + rayL) * Math.sin(a));
|
|
16
|
+
commands.push(shapes.drawLine(x1, y1, x2, y2, color));
|
|
17
|
+
}
|
|
18
|
+
return commands;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function drawShadedSphere(cx, cy, r, lightAngle) {
|
|
22
|
+
const commands = [];
|
|
23
|
+
|
|
24
|
+
// We simulate shading using overlapping crescent shapes or offset circles
|
|
25
|
+
// Since we have limited primitives, we'll draw concentric offset circles
|
|
26
|
+
// from dark to light towards the light source.
|
|
27
|
+
|
|
28
|
+
const offX = Math.cos(lightAngle);
|
|
29
|
+
const offY = Math.sin(lightAngle);
|
|
30
|
+
|
|
31
|
+
// 1. Cast Shadow on the ground
|
|
32
|
+
// Drawn first so it's behind the sphere
|
|
33
|
+
// Shift cast shadow away from light
|
|
34
|
+
const castR = Math.floor(r * 1.2);
|
|
35
|
+
const castRy = Math.floor(r * 0.3);
|
|
36
|
+
const castX = cx - Math.floor(offX * r * 0.8);
|
|
37
|
+
const castY = cy + r - Math.floor(castRy/2);
|
|
38
|
+
commands.push(shapes.drawEllipse(castX, castY, castR, castRy, '#424242', true));
|
|
39
|
+
|
|
40
|
+
// 2. Base Sphere (Reflected Light zone color)
|
|
41
|
+
// The very edge away from light gets some bounce light
|
|
42
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#78909c', true));
|
|
43
|
+
|
|
44
|
+
// 3. Core Shadow (main shadow zone)
|
|
45
|
+
// Offset slightly away from light
|
|
46
|
+
const coreR = Math.floor(r * 0.9);
|
|
47
|
+
const coreX = cx - Math.floor(offX * r * 0.1);
|
|
48
|
+
const coreY = cy - Math.floor(offY * r * 0.1);
|
|
49
|
+
commands.push(shapes.drawCircle(coreX, coreY, coreR, '#263238', true));
|
|
50
|
+
|
|
51
|
+
// 4. Halftone (mid-tone zone)
|
|
52
|
+
const halfR = Math.floor(r * 0.75);
|
|
53
|
+
const halfX = cx + Math.floor(offX * r * 0.1);
|
|
54
|
+
const halfY = cy + Math.floor(offY * r * 0.1);
|
|
55
|
+
commands.push(shapes.drawCircle(halfX, halfY, halfR, '#546e7a', true));
|
|
56
|
+
|
|
57
|
+
// 5. Light zone
|
|
58
|
+
const lightR = Math.floor(r * 0.5);
|
|
59
|
+
const lightX = cx + Math.floor(offX * r * 0.3);
|
|
60
|
+
const lightY = cy + Math.floor(offY * r * 0.3);
|
|
61
|
+
commands.push(shapes.drawCircle(lightX, lightY, lightR, '#90a4ae', true));
|
|
62
|
+
|
|
63
|
+
// 6. Highlight (bright spot)
|
|
64
|
+
const highR = Math.floor(r * 0.15);
|
|
65
|
+
const highX = cx + Math.floor(offX * r * 0.5);
|
|
66
|
+
const highY = cy + Math.floor(offY * r * 0.5);
|
|
67
|
+
commands.push(shapes.drawCircle(highX, highY, highR, '#ffffff', true));
|
|
68
|
+
|
|
69
|
+
return commands;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function drawShadedBox(cx, cy, w, h, lightAngle) {
|
|
73
|
+
const commands = [];
|
|
74
|
+
const d = Math.floor(w * 0.6);
|
|
75
|
+
const dx = Math.floor(d * 0.7);
|
|
76
|
+
const dy = Math.floor(d * 0.5);
|
|
77
|
+
|
|
78
|
+
const fX = cx - Math.floor(w/2);
|
|
79
|
+
const fY = cy + Math.floor(h/2);
|
|
80
|
+
const bX = fX + dx;
|
|
81
|
+
const bY = fY - dy;
|
|
82
|
+
|
|
83
|
+
// Assume light is from Top-Left or Top-Right
|
|
84
|
+
const isLightLeft = Math.cos(lightAngle) < 0;
|
|
85
|
+
|
|
86
|
+
// Determine values (colors) for planes based on light
|
|
87
|
+
// Top is usually Light (facing up towards light source)
|
|
88
|
+
const topColor = '#cfd8dc';
|
|
89
|
+
// Front and Side depend on left/right light
|
|
90
|
+
const frontColor = isLightLeft ? '#90a4ae' : '#455a64'; // If light is left, front gets some light. If right, front is darker.
|
|
91
|
+
const sideColor = isLightLeft ? '#455a64' : '#90a4ae';
|
|
92
|
+
|
|
93
|
+
// Cast Shadow
|
|
94
|
+
const castX = isLightLeft ? bX + w : fX - w;
|
|
95
|
+
const castY = fY;
|
|
96
|
+
// A simple polygon for cast shadow
|
|
97
|
+
const shadowPts = [
|
|
98
|
+
{x: fX, y: fY},
|
|
99
|
+
{x: fX + w, y: fY},
|
|
100
|
+
{x: castX, y: castY - dy},
|
|
101
|
+
{x: castX - w, y: castY - dy}
|
|
102
|
+
];
|
|
103
|
+
commands.push(shapes.drawPolyline(shadowPts, '#424242'));
|
|
104
|
+
|
|
105
|
+
// Draw solid planes
|
|
106
|
+
// Note: Since we don't have a fillPolygon tool natively exposed that supports all shapes easily,
|
|
107
|
+
// we will draw wireframes with heavy lines or crosshatching to represent values.
|
|
108
|
+
// Actually, we can just use drawRectangle for front.
|
|
109
|
+
|
|
110
|
+
// Front face (solid)
|
|
111
|
+
commands.push(shapes.drawRectangle(fX, fY - h, w, h, frontColor, true));
|
|
112
|
+
|
|
113
|
+
// Side face (Right) (wireframe/hatch for now, or just outline)
|
|
114
|
+
// Let's use outline with thick color
|
|
115
|
+
const rightSidePts = [
|
|
116
|
+
{x: fX+w, y: fY-h}, {x: bX+w, y: bY-h}, {x: bX+w, y: bY}, {x: fX+w, y: fY}
|
|
117
|
+
];
|
|
118
|
+
commands.push(shapes.drawPolyline(rightSidePts, sideColor));
|
|
119
|
+
// Add some hatch lines to simulate fill
|
|
120
|
+
commands.push(shapes.drawLine(fX+w, fY-Math.floor(h/2), bX+w, bY-Math.floor(h/2), sideColor));
|
|
121
|
+
|
|
122
|
+
// Top face
|
|
123
|
+
const topPts = [
|
|
124
|
+
{x: fX, y: fY-h}, {x: bX, y: bY-h}, {x: bX+w, y: bY-h}, {x: fX+w, y: fY-h}
|
|
125
|
+
];
|
|
126
|
+
commands.push(shapes.drawPolyline(topPts, topColor));
|
|
127
|
+
commands.push(shapes.drawLine(fX+Math.floor(w/2), fY-h, bX+Math.floor(w/2), bY-h, topColor));
|
|
128
|
+
|
|
129
|
+
return commands;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
drawLightSource,
|
|
134
|
+
drawShadedSphere,
|
|
135
|
+
drawShadedBox
|
|
136
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const lightLessons = require('./light_lessons');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register all lighting & shading tools on the MCP server.
|
|
7
|
+
* @param {McpServer} server
|
|
8
|
+
*/
|
|
9
|
+
function register(server) {
|
|
10
|
+
registerTool(server, 'art_tree_lesson_light_zones',
|
|
11
|
+
'Lesson: Lighting - 6 Zones of Light and Shadow on a Sphere',
|
|
12
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
13
|
+
async (p) => {
|
|
14
|
+
const commands = lightLessons.lessonLightZones(p.canvasSize);
|
|
15
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
16
|
+
return { content: [{ type: 'text', text: `✓ 6 Zones of Light lesson completed` }] };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
registerTool(server, 'art_tree_lesson_light_dir',
|
|
20
|
+
'Lesson: Lighting - Light Direction and Plane values on a Box',
|
|
21
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
22
|
+
async (p) => {
|
|
23
|
+
const commands = lightLessons.lessonLightDirection(p.canvasSize);
|
|
24
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
25
|
+
return { content: [{ type: 'text', text: `✓ Light Direction lesson completed` }] };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerTool(server, 'art_tree_lesson_light_contact',
|
|
29
|
+
'Lesson: Lighting - Contact Shadow (Darkest point of intersection)',
|
|
30
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
31
|
+
async (p) => {
|
|
32
|
+
const commands = lightLessons.lessonContactShadow(p.canvasSize);
|
|
33
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
34
|
+
return { content: [{ type: 'text', text: `✓ Contact Shadow lesson completed` }] };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
registerTool(server, 'art_tree_lesson_light_catalog',
|
|
38
|
+
'Get the catalog of all available Lighting lessons',
|
|
39
|
+
{},
|
|
40
|
+
() => {
|
|
41
|
+
const catalog = lightLessons.getLightLessonCatalog();
|
|
42
|
+
const text = catalog.map(l =>
|
|
43
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
44
|
+
).join('\n');
|
|
45
|
+
return { content: [{ type: 'text', text: `📚 Lighting Lessons:\n\n${text}` }] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { register };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const materialShapes = require('./material_shapes');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lesson: Shiny vs Matte (Metal vs Plastic/Rubber)
|
|
5
|
+
* Compares high contrast sharp highlights vs low contrast diffused highlights.
|
|
6
|
+
*/
|
|
7
|
+
function lessonMaterialShiny(canvasSize = 32) {
|
|
8
|
+
const commands = [];
|
|
9
|
+
const cx1 = Math.floor(canvasSize * 0.3);
|
|
10
|
+
const cx2 = Math.floor(canvasSize * 0.7);
|
|
11
|
+
const cy = Math.floor(canvasSize / 2);
|
|
12
|
+
const r = Math.floor(canvasSize * 0.2);
|
|
13
|
+
|
|
14
|
+
// 1. Metal Sphere (Left)
|
|
15
|
+
commands.push(...materialShapes.drawMetalSphere(cx1, cy, r));
|
|
16
|
+
|
|
17
|
+
// 2. Matte/Plastic Sphere (Right)
|
|
18
|
+
commands.push(...materialShapes.drawMatteSphere(cx2, cy, r, '#e65100'));
|
|
19
|
+
|
|
20
|
+
return commands;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lesson: Transparent (Glass)
|
|
25
|
+
* Demonstrates Fresnel effect, sharp reflections, and light transmission.
|
|
26
|
+
*/
|
|
27
|
+
function lessonMaterialGlass(canvasSize = 32) {
|
|
28
|
+
const commands = [];
|
|
29
|
+
const cx = Math.floor(canvasSize / 2);
|
|
30
|
+
const cy = Math.floor(canvasSize / 2);
|
|
31
|
+
const r = Math.floor(canvasSize * 0.35);
|
|
32
|
+
|
|
33
|
+
commands.push(...materialShapes.drawGlassSphere(cx, cy, r));
|
|
34
|
+
|
|
35
|
+
return commands;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lesson: Texture (Wood vs Stone)
|
|
40
|
+
* Compares organic lines vs rough cracks.
|
|
41
|
+
*/
|
|
42
|
+
function lessonMaterialTexture(canvasSize = 32) {
|
|
43
|
+
const commands = [];
|
|
44
|
+
const cx1 = Math.floor(canvasSize * 0.3);
|
|
45
|
+
const cx2 = Math.floor(canvasSize * 0.7);
|
|
46
|
+
const cy = Math.floor(canvasSize / 2);
|
|
47
|
+
const r = Math.floor(canvasSize * 0.2);
|
|
48
|
+
|
|
49
|
+
// 1. Wood Sphere (Left)
|
|
50
|
+
commands.push(...materialShapes.drawTexturedSphere(cx1, cy, r, 'wood'));
|
|
51
|
+
|
|
52
|
+
// 2. Stone Sphere (Right)
|
|
53
|
+
commands.push(...materialShapes.drawTexturedSphere(cx2, cy, r, 'stone'));
|
|
54
|
+
|
|
55
|
+
return commands;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getMaterialLessonCatalog() {
|
|
59
|
+
return [
|
|
60
|
+
{ id: 'material_shiny', name: 'Metal vs Matte', description: 'Sharp highlights (Metal) vs Soft highlights (Plastic)', fn: lessonMaterialShiny },
|
|
61
|
+
{ id: 'material_glass', name: 'Glass & Transparency', description: 'Fresnel effect and transmitted light', fn: lessonMaterialGlass },
|
|
62
|
+
{ id: 'material_texture', name: 'Texture: Wood vs Stone', description: 'Grain lines vs cracks on a 3D form', fn: lessonMaterialTexture },
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
lessonMaterialShiny,
|
|
68
|
+
lessonMaterialGlass,
|
|
69
|
+
lessonMaterialTexture,
|
|
70
|
+
getMaterialLessonCatalog
|
|
71
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
function drawMetalSphere(cx, cy, r) {
|
|
4
|
+
const commands = [];
|
|
5
|
+
|
|
6
|
+
// Base sphere (darkest shadow/environment reflection)
|
|
7
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#263238', true));
|
|
8
|
+
|
|
9
|
+
// High contrast core shadow
|
|
10
|
+
const coreR = Math.floor(r * 0.9);
|
|
11
|
+
commands.push(shapes.drawCircle(cx, cy + Math.floor(r*0.1), coreR, '#1a1a1a', true));
|
|
12
|
+
|
|
13
|
+
// Environment reflection (Ground bounce)
|
|
14
|
+
const envR = Math.floor(r * 0.7);
|
|
15
|
+
commands.push(shapes.drawCircle(cx, cy + Math.floor(r*0.3), envR, '#546e7a', true));
|
|
16
|
+
|
|
17
|
+
// Sharp light zone
|
|
18
|
+
const lightR = Math.floor(r * 0.6);
|
|
19
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.2), cy - Math.floor(r*0.2), lightR, '#cfd8dc', true));
|
|
20
|
+
|
|
21
|
+
// VERY sharp highlight (Tiny and pure white)
|
|
22
|
+
const highR = Math.floor(r * 0.1);
|
|
23
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.4), cy - Math.floor(r*0.4), highR, '#ffffff', true));
|
|
24
|
+
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function drawMatteSphere(cx, cy, r, colorBase) {
|
|
29
|
+
const commands = [];
|
|
30
|
+
|
|
31
|
+
// Base sphere (shadow)
|
|
32
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#795548', true)); // Dark brownish shadow
|
|
33
|
+
|
|
34
|
+
// Smooth gradation (Halftone)
|
|
35
|
+
const halfR = Math.floor(r * 0.85);
|
|
36
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.05), cy - Math.floor(r*0.05), halfR, colorBase, true));
|
|
37
|
+
|
|
38
|
+
// Light area
|
|
39
|
+
const lightR = Math.floor(r * 0.6);
|
|
40
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.15), cy - Math.floor(r*0.15), lightR, '#ffcc80', true)); // Lighter tone
|
|
41
|
+
|
|
42
|
+
// Soft highlight (Large and diffused)
|
|
43
|
+
const highR = Math.floor(r * 0.3);
|
|
44
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.25), cy - Math.floor(r*0.25), highR, '#ffe0b2', true));
|
|
45
|
+
|
|
46
|
+
return commands;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function drawGlassSphere(cx, cy, r) {
|
|
50
|
+
const commands = [];
|
|
51
|
+
|
|
52
|
+
// Fresnel effect: Edges are brighter/more reflective than the center
|
|
53
|
+
// Base (Outline & Rim light)
|
|
54
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#80deea', true));
|
|
55
|
+
|
|
56
|
+
// Darker inner area (light passes through)
|
|
57
|
+
const innerR = Math.floor(r * 0.9);
|
|
58
|
+
commands.push(shapes.drawCircle(cx, cy, innerR, '#006064', true));
|
|
59
|
+
|
|
60
|
+
// Transmitted light hitting the back bottom
|
|
61
|
+
const transR = Math.floor(r * 0.7);
|
|
62
|
+
commands.push(shapes.drawCircle(cx + Math.floor(r*0.1), cy + Math.floor(r*0.2), transR, '#4dd0e1', true));
|
|
63
|
+
|
|
64
|
+
// Very center is darkest (least reflection)
|
|
65
|
+
const centerR = Math.floor(r * 0.5);
|
|
66
|
+
commands.push(shapes.drawCircle(cx, cy, centerR, '#00363a', true));
|
|
67
|
+
|
|
68
|
+
// Sharp Highlights (Primary and secondary reflections)
|
|
69
|
+
const highR1 = Math.floor(r * 0.15);
|
|
70
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.4), cy - Math.floor(r*0.4), highR1, '#ffffff', true));
|
|
71
|
+
|
|
72
|
+
const highR2 = Math.floor(r * 0.08);
|
|
73
|
+
commands.push(shapes.drawCircle(cx + Math.floor(r*0.3), cy + Math.floor(r*0.4), highR2, '#e0f7fa', true));
|
|
74
|
+
|
|
75
|
+
return commands;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function drawTexturedSphere(cx, cy, r, type) {
|
|
79
|
+
const commands = [];
|
|
80
|
+
|
|
81
|
+
// Base sphere
|
|
82
|
+
const baseColor = type === 'wood' ? '#8d6e63' : '#9e9e9e';
|
|
83
|
+
commands.push(shapes.drawCircle(cx, cy, r, baseColor, true));
|
|
84
|
+
|
|
85
|
+
// Shadow
|
|
86
|
+
const shadowColor = type === 'wood' ? '#4e342e' : '#616161';
|
|
87
|
+
const shadowR = Math.floor(r * 0.85);
|
|
88
|
+
commands.push(shapes.drawCircle(cx + Math.floor(r*0.1), cy + Math.floor(r*0.1), shadowR, shadowColor, true));
|
|
89
|
+
|
|
90
|
+
// Texture application
|
|
91
|
+
if (type === 'wood') {
|
|
92
|
+
// Wood grain lines curving around the sphere
|
|
93
|
+
const grainColor = '#3e2723';
|
|
94
|
+
for (let i = -2; i <= 2; i++) {
|
|
95
|
+
const rx = r * 0.8;
|
|
96
|
+
const ry = r * 0.3 * Math.abs(i) + r * 0.2;
|
|
97
|
+
commands.push(shapes.drawEllipse(cx, cy + (i * Math.floor(r*0.3)), Math.floor(rx), Math.floor(ry), grainColor, false));
|
|
98
|
+
}
|
|
99
|
+
} else if (type === 'stone') {
|
|
100
|
+
// Stone cracks and rough patches
|
|
101
|
+
const crackColor = '#212121';
|
|
102
|
+
// Simplified cracks using polylines
|
|
103
|
+
commands.push(shapes.drawPolyline([
|
|
104
|
+
{x: cx - Math.floor(r*0.5), y: cy - Math.floor(r*0.5)},
|
|
105
|
+
{x: cx - Math.floor(r*0.2), y: cy - Math.floor(r*0.1)},
|
|
106
|
+
{x: cx + Math.floor(r*0.3), y: cy - Math.floor(r*0.2)},
|
|
107
|
+
{x: cx + Math.floor(r*0.6), y: cy + Math.floor(r*0.1)}
|
|
108
|
+
], crackColor));
|
|
109
|
+
|
|
110
|
+
commands.push(shapes.drawPolyline([
|
|
111
|
+
{x: cx - Math.floor(r*0.2), y: cy - Math.floor(r*0.1)},
|
|
112
|
+
{x: cx, y: cy + Math.floor(r*0.4)},
|
|
113
|
+
{x: cx - Math.floor(r*0.4), y: cy + Math.floor(r*0.7)}
|
|
114
|
+
], crackColor));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Soft Highlight over texture
|
|
118
|
+
const highColor = type === 'wood' ? '#d7ccc8' : '#e0e0e0';
|
|
119
|
+
const highR = Math.floor(r * 0.4);
|
|
120
|
+
commands.push(shapes.drawCircle(cx - Math.floor(r*0.3), cy - Math.floor(r*0.3), highR, highColor, true));
|
|
121
|
+
|
|
122
|
+
return commands;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
drawMetalSphere,
|
|
127
|
+
drawMatteSphere,
|
|
128
|
+
drawGlassSphere,
|
|
129
|
+
drawTexturedSphere
|
|
130
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const materialLessons = require('./material_lessons');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register all material & surface rendering tools on the MCP server.
|
|
7
|
+
* @param {McpServer} server
|
|
8
|
+
*/
|
|
9
|
+
function register(server) {
|
|
10
|
+
registerTool(server, 'art_tree_lesson_material_shiny',
|
|
11
|
+
'Lesson: Materials - Metal vs Matte (High contrast vs diffused highlights)',
|
|
12
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
13
|
+
async (p) => {
|
|
14
|
+
const commands = materialLessons.lessonMaterialShiny(p.canvasSize);
|
|
15
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
16
|
+
return { content: [{ type: 'text', text: `✓ Metal vs Matte material lesson completed` }] };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
registerTool(server, 'art_tree_lesson_material_glass',
|
|
20
|
+
'Lesson: Materials - Glass (Transparency, Fresnel effect)',
|
|
21
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
22
|
+
async (p) => {
|
|
23
|
+
const commands = materialLessons.lessonMaterialGlass(p.canvasSize);
|
|
24
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
25
|
+
return { content: [{ type: 'text', text: `✓ Glass material lesson completed` }] };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerTool(server, 'art_tree_lesson_material_texture',
|
|
29
|
+
'Lesson: Materials - Texture (Wood grain vs Stone cracks)',
|
|
30
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
31
|
+
async (p) => {
|
|
32
|
+
const commands = materialLessons.lessonMaterialTexture(p.canvasSize);
|
|
33
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
34
|
+
return { content: [{ type: 'text', text: `✓ Wood vs Stone texture lesson completed` }] };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
registerTool(server, 'art_tree_lesson_material_catalog',
|
|
38
|
+
'Get the catalog of all available Material lessons',
|
|
39
|
+
{},
|
|
40
|
+
() => {
|
|
41
|
+
const catalog = materialLessons.getMaterialLessonCatalog();
|
|
42
|
+
const text = catalog.map(l =>
|
|
43
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
44
|
+
).join('\n');
|
|
45
|
+
return { content: [{ type: 'text', text: `📚 Material Lessons:\n\n${text}` }] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { register };
|